├── .babelrc ├── .browserslistrc ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── commit.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── LICENSE ├── README.md ├── commitlint.config.js ├── demo ├── copyBuild.sh ├── demo.js ├── index.css └── index.html ├── docs ├── .nojekyll ├── CNAME ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ └── RinzlerEngine.html ├── index.html ├── modules.html └── types │ ├── WorkerFunction.html │ ├── WorkerFunctionTransferArgs.html │ └── WorkerInitFunction.html ├── media ├── infographic.png ├── rinzler_cover.png ├── rinzler_logo.png ├── rinzler_logo.svg └── rinzler_logo_text.svg ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── event-emitter.ts ├── index.ts ├── median.ts ├── scheduler.ts ├── tsconfig.json └── worker-wrapper.ts ├── tsconfig.json ├── typedoc.json └── worker-src ├── index.d.ts ├── index.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "targets": { "esmodules": true } 5 | }], 6 | "@babel/typescript" 7 | ], 8 | "plugins": [ 9 | "@babel/plugin-proposal-class-properties" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.5% and supports webworkers and supports promises and not dead 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true, 8 | "impliedStrict": true 9 | } 10 | }, 11 | "plugins": [ 12 | "@typescript-eslint" 13 | ], 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "env": { 19 | "node": true, 20 | "es2020": true 21 | }, 22 | "rules": { 23 | "indent": [ 24 | 2, 25 | "tab", 26 | { 27 | "SwitchCase": 1 28 | } 29 | ], 30 | "semi": [ 31 | 2, 32 | "never" 33 | ], 34 | "curly": [ 35 | 2, 36 | "multi-line" 37 | ], 38 | "linebreak-style": [ 39 | 2, 40 | "unix" 41 | ], 42 | "quotes": [ 43 | 2, 44 | "single", 45 | { 46 | "avoidEscape": true, 47 | "allowTemplateLiterals": true 48 | } 49 | ], 50 | "no-warning-comments": 1, 51 | "object-curly-spacing": [ 52 | 1, 53 | "always" 54 | ], 55 | "array-bracket-spacing": [ 56 | 1, 57 | "never" 58 | ], 59 | "no-await-in-loop": 0, 60 | "@typescript-eslint/member-delimiter-style": [ 61 | 2, 62 | { 63 | "multiline": { 64 | "delimiter": "none" 65 | }, 66 | "singleline": { 67 | "delimiter": "semi", 68 | "requireLast": false 69 | } 70 | } 71 | ], 72 | "@typescript-eslint/naming-convention": [ 73 | 1, 74 | { 75 | "selector": "default", 76 | "format": [ 77 | "camelCase" 78 | ], 79 | "leadingUnderscore": "allow", 80 | "trailingUnderscore": "allow" 81 | }, 82 | { 83 | "selector": "variable", 84 | "format": [ 85 | "camelCase", 86 | "UPPER_CASE" 87 | ], 88 | "leadingUnderscore": "allow", 89 | "trailingUnderscore": "allow" 90 | }, 91 | { 92 | "selector": "property", 93 | "format": [ 94 | "camelCase", 95 | "UPPER_CASE" 96 | ], 97 | "leadingUnderscore": "allow", 98 | "trailingUnderscore": "allow" 99 | }, 100 | { 101 | "selector": "memberLike", 102 | "modifiers": [ 103 | "private" 104 | ], 105 | "format": [ 106 | "camelCase" 107 | ], 108 | "leadingUnderscore": "require" 109 | }, 110 | { 111 | "selector": "typeLike", 112 | "format": [ 113 | "PascalCase" 114 | ] 115 | } 116 | ] 117 | }, 118 | "overrides": [ 119 | { 120 | "files": [ 121 | "*.js" 122 | ], 123 | "rules": { 124 | "@typescript-eslint/no-var-requires": 0 125 | } 126 | } 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: GitSquared 4 | custom: ['https://gaby.dev/donate'] 5 | -------------------------------------------------------------------------------- /.github/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | name: Commit Checks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 12.x 14 | - name: Cache dependencies 15 | uses: actions/cache@v2 16 | with: 17 | path: | 18 | **/node_modules 19 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Run lint script 23 | run: npm run lint 24 | build: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: 12.x 32 | - name: Cache dependencies 33 | uses: actions/cache@v2 34 | with: 35 | path: | 36 | **/node_modules 37 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 38 | - name: Install dependencies 39 | run: npm ci 40 | - name: Run build 41 | run: npm run build 42 | test: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: actions/setup-node@v1 48 | with: 49 | node-version: 12.x 50 | - name: Cache dependencies 51 | uses: actions/cache@v2 52 | with: 53 | path: | 54 | **/node_modules 55 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 56 | - name: Install dependencies 57 | run: npm ci 58 | - name: Run build 59 | run: npm run build 60 | - name: Run tests 61 | run: npm run test 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | demo/dist 2 | .vercel 3 | 4 | .DS_Store 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | out 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and not Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | 113 | # Stores VSCode versions used for testing VSCode extensions 114 | .vscode-test 115 | 116 | # yarn v2 117 | .yarn/cache 118 | .yarn/unplugged 119 | .yarn/build-state.yml 120 | .yarn/install-state.gz 121 | .pnp.* 122 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install komit $1 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2020-2021 Gabriel Saillard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Rinzler project logo 3 |

4 | npm version badge 5 | types included badge 6 | license badge 7 |


8 |
9 | 10 | [Rinzler](https://github.com/GitSquared/rinzler) is a ~~turboramjet~~ **parallel processing engine** for the browser. 11 | 12 | It speeds up your web application by allowing recurring functions to execute in parallel taking full advantage of the host system's available processing power. 13 | 14 | Check out the [full docs](https://gitsquared.github.io/rinzler/classes/rinzlerengine.html), try the [interactive demo](https://rinzler-demo.vercel.app) or read on for a high-level overview and a quick start guide. 15 | 16 | ## Concept 17 | Most devices have a processor unit (CPU) with multiple *cores*, meaning that they are capable of working on multiple tasks at the same time. 18 | Modern operating systems with multi-tasking functionality (e.g the ability to run & manage multiple programs/windows) have a special component called a *thread scheduler*. 19 | 20 | Each program you run can have multiple *threads*, and the scheduler's job is to distribute threads to the CPU's cores. 21 | 22 | A web page's JavaScript normally executes on a single thread, meaning it will never use more than one CPU core. In most cases this is fine, and also helps ensures other tabs in the user's browser, or other programs, can also keep running smoothly. 23 | 24 | However, some web applications might need to process a lot of data, or do a lot of expensive computing, and therefore can benefit from spreading work across all the available cores of the host machine. 25 | 26 | Rinzler is a tool to do just that, in the simplest way possible - just define functions to run in parallel, and use native ES6 [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) to run & manage parallelized jobs. 27 | 28 |
29 | Infographic explaining how Rinzler allows you to use more CPU cores 30 |
31 | 32 | Internally, it leverages [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API), which is a standard Web API for executing code in separate threads. It also includes a custom scheduler that handles spawning new Workers when necessary, sharing work between them, and shutting them down when they're not used. 33 | 34 | ## Install 35 | ``` 36 | npm i rinzler-engine 37 | ``` 38 | 39 | Both ES & UMD modules are bundled, as well as TypeScript types, so you should be all set. 40 | 41 | Rinzler targets browsers with [WebWorkers](https://caniuse.com/webworkers) and [Promises](https://caniuse.com/promises) support (check the [browserslistrc](https://github.com/GitSquared/rinzler/raw/master/.browserslistrc)). Most modern evergreen browsers, including Edge, should be compatible. 42 | 43 | ## Quick start 44 | In the following example, we will set up a Rinzler-accelerated app that decodes [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)s of text. 45 | 46 | ### 0. Environment initialization (optional) 47 | In most real-life use cases, the job processing you will offload to Rinzler will depend on some dynamic variable in the context of your app: in this example, the original encoding of the text we want to decode. 48 | 49 | The processing functions you will pass to Rinzler ***cannot contain references to external variables***, because their source code will be extracted and printed in the Worker instances' source. 50 | 51 | To work around this limitation, Rinzler allows you to setup an "initialization" function and pass it a payload. This function & payload will be run on each new Web Worker instance before it starts processing your jobs. 52 | 53 | ```js 54 | const initOptions = [{ 55 | encoding: 'utf-8' // We're just going to print this here, but in real life you would probably get this option from user input. 56 | }] 57 | 58 | function init(options) { 59 | // This will run once in new Web Worker contexts. We can use the `self` global to store data for later. 60 | self.params = { 61 | encoding: options.encoding 62 | } 63 | } 64 | ``` 65 | 66 | ### 1. Job processing function 67 | We need to setup a function that will actually do the job we need to parallelize, in this case, decoding text buffers. 68 | 69 | ```js 70 | function processJob(message) { 71 | // We expect to receive an object with an `encodedText` prop that is an ArrayBuffer. 72 | const buffer = message.encodedText 73 | 74 | // Get the encoding parameter we stored earlier, or default to ASCII. 75 | const encoding = self.params?.encoding || 'ascii' 76 | 77 | const text = new TextDecoder(encoding).decode(buffer) 78 | return [text] 79 | } 80 | ``` 81 | 82 | ### 2. Engine start 83 | Next we will import Rinzler and start the engine by passing the function(s) we defined above. 84 | 85 | The following code is written for asynchronous contexts, but you can translate it to synchronous by using `.then()` with a callback instead of `await`. 86 | 87 | ```js 88 | import RinzlerEngine from 'rinzler-engine' 89 | 90 | const engine = await new RinzlerEngine().configureAndStart(processJob, init, initOptions) 91 | ``` 92 | 93 | ### 3. Running jobs 94 | Now we can actually run jobs! We'll use the `runJob()` method, which returns a `Promise` that will resolve when the job is completed. 95 | 96 | Since we need to pass an `ArrayBuffer`, we'll use the second argument as a `Transferable[]` - much like in the native [`worker.postMessage()` API.](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) 97 | 98 | ```js 99 | // Encode some text to try our decoder with 100 | const encodedText = new TextEncoder('utf-8').encode('hello Rinzler!') 101 | 102 | // Pass the encoded text to our decoding engine 103 | const decodedResult = await engine.runJob({ encodedText }, [encodedText]) 104 | 105 | console.log(decodedResult) // "hello Rinzler!" 106 | ``` 107 | 108 | You can start as many jobs as you want, and take full advantage of ES6's asynchronous syntax (for example, [`Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)). 109 | 110 | **If you use TypeScript,** you can pass return types with the `runJob(): Promise` signature. 111 | 112 | Under the hood, Rinzler will take care of launching Web Workers, balancing their load, and gracefully shutting them down when needed to reduce your app's memory footprint. 113 | 114 | ### 4. Shutting down 115 | Web Worker instances will be destroyed by the browser when the page exits, but you can schedule a graceful shutdown yourself using `engine.shutdown()`, which returns a `Promise` that will resolve once all currently active jobs have completed and all workers have been stopped. 116 | 117 | ## Licensing 118 | Rinzler is licensed under the [MIT License](https://github.com/GitSquared/rinzler/blob/master/LICENSE). You may integrate it in commercial applications. 119 | 120 | --- 121 | 122 | ###### © 2020-2022 Gabriel Saillard 123 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /demo/copyBuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -d "dist" ]; then 4 | rm -Rf dist 5 | fi 6 | 7 | mkdir dist 8 | cp -R ../dist/* dist/ 9 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | function setEngineFan(percent) { 2 | const speed = (percent === -1) ? 0 : 7 - (6.7 * percent) 3 | document.querySelector('#fans').style.setProperty('animation-duration', `${speed}s`) 4 | } 5 | 6 | function setEngineFlame(percent) { 7 | if (percent === -1) percent = 0 8 | const size = 0.6 + (0.6 * percent) 9 | document.querySelector('#flame').style.setProperty('--size', `${size}`) 10 | } 11 | 12 | function setEngineShake(percent) { 13 | if (percent === -1) percent = 0 14 | const x = 5 * percent 15 | const y = 2 * percent 16 | 17 | const reactor = document.querySelector('#reactor') 18 | 19 | for (const prop of ['x', '-x', 'y', '-y']) { 20 | let value 21 | if (prop.includes('x')) { 22 | value = x 23 | } else { 24 | value = y 25 | } 26 | if (prop.includes('-')) { 27 | value = -value 28 | } 29 | reactor.style.setProperty(`--shake${prop}`, `${value}px`) 30 | } 31 | } 32 | 33 | function visualizeEngineLoad(percent) { 34 | setEngineFan(percent) 35 | setEngineFlame(percent) 36 | // setEngineShake(percent) 37 | } 38 | 39 | function log(s) { 40 | const log = document.createElement('span') 41 | const now = new Date() 42 | log.innerText = `[${now.getMinutes()}:${now.getSeconds()}] ${s}` 43 | document.querySelector('#log-box').prepend(log) 44 | } 45 | 46 | function fakeLoad(n) { 47 | const start = self.performance.now() 48 | let a = 0 49 | while(self.performance.now() - start < n) { 50 | a = Math.random() * 1000 / Math.random() * 1000 51 | } 52 | return [a] 53 | } 54 | 55 | function startFakeLoadJobs(n, l) { 56 | if (window.loadInterval) clearInterval(window.loadInterval) 57 | window.loadInterval = setInterval(() => { 58 | engine.runJob(l) 59 | }, n) 60 | document.querySelector('#load-monitor').innerText = `${l}ms job every ${n}ms` 61 | log(`fake load generator: submitting a ${l}ms job every ${n}ms to engine`) 62 | } 63 | 64 | function stopFakeLoadJobs() { 65 | clearInterval(window.loadInterval) 66 | window.loadInterval = null 67 | document.querySelector('#load-monitor').innerText = 'no fake load' 68 | log('fake load generator: full stop') 69 | } 70 | 71 | function startSensorWatcher() { 72 | if (window.senseInterval) clearInterval(window.senseInterval) 73 | const pr = document.querySelector('#sensors-print') 74 | visualizeEngineLoad(0) 75 | window.senseInterval = setInterval(() => { 76 | const deb = window.engine._debug() 77 | pr.innerText = 78 | `min / max temp: ${deb.minTemp} / ${deb.maxTemp}\n` + 79 | `temp (target): ${deb.temp} (${deb.targetTemp})\n` + 80 | `pressure: ${deb.scheduler.pressure}\n` + 81 | `cooling delay: ${deb.coolingDelay}ms\n` + 82 | `median spinup: ${deb.medianExtendPoolTime}ms\n` + 83 | `median job: ${deb.scheduler.measureMedianExecTime()}ms\n` 84 | updateWviewer(deb.scheduler.workerPool, deb.coolingTimer, deb.coolingDelay) 85 | visualizeEngineLoad(deb.temp / deb.maxTemp) 86 | setEngineShake(deb.scheduler.pressure / (deb.maxTemp * 3)) 87 | }, 100) 88 | } 89 | 90 | function stopSensorWatcher() { 91 | clearInterval(window.senseInterval) 92 | document.querySelector('#sensors-print').innerText = '-no data-' 93 | document.querySelector('#wviewer').innerHTML = '' 94 | visualizeEngineLoad(-1) 95 | setEngineShake(0) 96 | } 97 | 98 | const timeout = window.setTimeout 99 | window.lastTimeoutStart = 0 100 | window.setTimeout = function(f, t) { 101 | window.lastTimeoutStart = Date.now() 102 | return timeout(f, t) 103 | } 104 | 105 | function makeSpan(text) { 106 | let s = document.createElement('span') 107 | s.innerText = text 108 | return s 109 | } 110 | 111 | let prevW = 0 112 | function updateWviewer(pool, ct, cd) { 113 | const v = document.querySelector('#wviewer') 114 | v.innerHTML = '' 115 | pool.forEach((w, wid) => { 116 | const box = document.createElement('div') 117 | box.classList.add('worker') 118 | box.appendChild(makeSpan(`${wid}`)) 119 | box.appendChild(makeSpan('running:')) 120 | box.appendChild(makeSpan(`${(w.jobs[0] && w.jobs[0].id) || 'idle'}`)) 121 | box.appendChild(makeSpan('in queue:')) 122 | box.appendChild(makeSpan(`${(w.jobs.length === 0) ? '0' : w.jobs.length - 1}`)) 123 | if (ct && ct[1] === wid) { 124 | box.appendChild(makeSpan(`cooldown: ${Math.round((cd - (Date.now() - window.lastTimeoutStart))/100)/10}s`)) 125 | } 126 | v.appendChild(box) 127 | }) 128 | if (pool.size !== prevW) { 129 | if (pool.size > prevW) { 130 | log(`workers watcher: detected ${pool.size - prevW} new worker(s)`) 131 | } else { 132 | log(`workers watcher: ${prevW - pool.size} worker(s) terminated`) 133 | } 134 | prevW = pool.size 135 | } 136 | } 137 | 138 | function linkControlBox() { 139 | const starter = document.querySelector('#power-button') 140 | const heater = document.querySelector('#heat-button') 141 | const freqSlider = document.querySelector('#load-freq') 142 | const weiSlider = document.querySelector('#load-wei') 143 | freqSlider.disabled = true 144 | freqSlider.value = 0 145 | weiSlider.disabled = true 146 | weiSlider.value = 0 147 | 148 | let started = false 149 | window.engine 150 | 151 | async function start() { 152 | log('starting engine...') 153 | window.engine = await new RinzlerEngine().configureAndStart(fakeLoad) 154 | starter.innerText = 'Stop' 155 | starter.dataset.highlight = 'false' 156 | heater.disabled = false 157 | freqSlider.disabled = false 158 | weiSlider.disabled = false 159 | startSensorWatcher() 160 | started = true 161 | } 162 | 163 | async function stop() { 164 | stopFakeLoadJobs() 165 | log('stopping engine...') 166 | await window.engine.shutdown() 167 | starter.innerText = 'Start' 168 | starter.dataset.highlight = 'true' 169 | heater.disabled = true 170 | freqSlider.disabled = true 171 | freqSlider.value = 0 172 | weiSlider.disabled = true 173 | weiSlider.value = 0 174 | stopSensorWatcher() 175 | started = false 176 | } 177 | 178 | starter.addEventListener('click', () => { 179 | return (!started) ? start() : stop() 180 | }) 181 | 182 | heater.addEventListener('click', () => { 183 | log('heating up engine...') 184 | window.engine.preHeat() 185 | }) 186 | 187 | function updateFakeLoadParams() { 188 | let pct = freqSlider.value / 100 189 | let pctW = weiSlider.value / 100 190 | if (pct === 0) { 191 | stopFakeLoadJobs() 192 | } else { 193 | let n = 2000 - (1900 * pct) 194 | let l = 100 + (1900 * pctW) 195 | startFakeLoadJobs(n, l) 196 | } 197 | visualizeEngineLoad(pct) 198 | } 199 | 200 | freqSlider.addEventListener('change', updateFakeLoadParams.bind(this)) 201 | weiSlider.addEventListener('change', updateFakeLoadParams.bind(this)) 202 | } 203 | linkControlBox() 204 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | margin: 0; 7 | padding: 0; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | 12 | body { 13 | display: grid; 14 | grid-template: 15 | "reactor-container control-box sensors-box" minmax(200px, 25%) 16 | "log-box wviewer wviewer" 1fr / 370px 1fr 1fr; 17 | background: #f3f3f3; 18 | font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 19 | } 20 | 21 | body > div { 22 | border: 3px solid #3c3c3c; 23 | border-top: none; 24 | position: relative; 25 | padding-top: 30px; 26 | } 27 | 28 | #reactor-container { 29 | height: 100%; 30 | width: 100%; 31 | grid-area: reactor-container; 32 | display: flex; 33 | border-right: none; 34 | padding: 15px; 35 | } 36 | 37 | #reactor-container::before { 38 | content: "Load visualization"; 39 | position: absolute; 40 | font-size: 13px; 41 | font-weight: bold; 42 | text-align: center; 43 | top: 0; 44 | left: 0; 45 | right: 0; 46 | } 47 | 48 | @keyframes shake { 49 | from { 50 | transform: translate3d(var(--shakex), var(--shakey), 0); 51 | } 52 | to { 53 | transform: translate3d(var(--shake-x), var(--shake-y), 0); 54 | } 55 | } 56 | 57 | #reactor { 58 | --shakex: 0px; 59 | --shake-x: -0px; 60 | --shakey: 0px; 61 | --shake-y: -0px; 62 | animation: shake .04s linear alternate infinite; 63 | margin: auto; 64 | width: 100%; 65 | height: 100%; 66 | } 67 | 68 | @keyframes spin { 69 | from { 70 | transform: scale3d(1.07,1.07,1.07) rotate3d(0,0,1, 0deg); 71 | } 72 | to { 73 | transform: scale3d(1.07,1.07,1.07) rotate3d(0,0,1, 360deg); 74 | } 75 | } 76 | 77 | #fans { 78 | transform-box: fill-box; 79 | transform-origin: center; 80 | animation: spin 0s linear infinite; 81 | } 82 | 83 | @keyframes flame { 84 | from { fill: #f54100 } 85 | to { fill: #ff8000 } 86 | } 87 | 88 | #flame { 89 | --size: 0.6; 90 | transform-box: fill-box; 91 | transform-origin: right; 92 | /* between 0.6 and 1.2 */ 93 | transform: scale3d(var(--size), 1, 1); 94 | animation: flame .3s ease-out alternate infinite; 95 | transition: transform .3s ease-out; 96 | } 97 | 98 | #control-box { 99 | grid-area: control-box; 100 | display: flex; 101 | flex-direction: column; 102 | align-items: center; 103 | justify-content: center; 104 | background: white; 105 | } 106 | 107 | #control-box::before { 108 | content: "Control panel"; 109 | position: absolute; 110 | font-size: 13px; 111 | font-weight: bold; 112 | text-align: center; 113 | top: 0; 114 | left: 0; 115 | right: 0; 116 | } 117 | 118 | #main-buttons button { 119 | padding: 5px 10px; 120 | font-size: 14px; 121 | font-weight: bold; 122 | color: white; 123 | background: #3c3c3c; 124 | border: none; 125 | border-radius: 6px; 126 | outline: none; 127 | } 128 | 129 | #main-buttons button:disabled { 130 | cursor: not-allowed; 131 | opacity: 0.6; 132 | } 133 | 134 | #main-buttons button:not(:disabled):hover { 135 | box-shadow: 0px 1px 4px rgba(0,0,0,0.3); 136 | } 137 | 138 | #main-buttons button:not(:disabled):focus { 139 | box-shadow: inset 0px -1px 4px rgba(0,0,0,0.3); 140 | } 141 | 142 | #power-button[data-highlight="true"] { 143 | background: #fa6000; 144 | } 145 | 146 | button, input[type="range"] { 147 | cursor: pointer; 148 | } 149 | 150 | #load-controller { 151 | display: flex; 152 | flex-direction: column; 153 | border: 2px solid #3c3c3c; 154 | border-radius: 5px; 155 | padding: 15px; 156 | margin: 15px; 157 | } 158 | 159 | label, span { 160 | font-size: 12px; 161 | } 162 | 163 | #load-monitor { 164 | margin-top: 5px; 165 | text-align: center; 166 | opacity: 0.6; 167 | } 168 | 169 | #sensors-box { 170 | grid-area: sensors-box; 171 | border-left: none; 172 | background: white; 173 | padding: 15px; 174 | display: flex; 175 | flex-direction: column; 176 | align-items: center; 177 | justify-content: center; 178 | } 179 | 180 | #sensors-box::before { 181 | content: "Engine sensors"; 182 | position: absolute; 183 | font-size: 13px; 184 | font-weight: bold; 185 | text-align: center; 186 | top: 0; 187 | left: 0; 188 | right: 0; 189 | } 190 | 191 | #sensors-print { 192 | font-size: 12px; 193 | } 194 | 195 | #log-box { 196 | grid-area: log-box; 197 | border-right: none; 198 | color: white; 199 | background: #282828; 200 | display: flex; 201 | flex-direction: column-reverse; 202 | overflow: auto; 203 | padding: 0 7px; 204 | } 205 | 206 | #log-box::before { 207 | content: "Logs"; 208 | position: absolute; 209 | font-size: 13px; 210 | font-weight: bold; 211 | text-align: center; 212 | top: 0; 213 | left: 0; 214 | right: 0; 215 | padding-bottom: 15px; 216 | background: linear-gradient(to top, transparent, #282828); 217 | } 218 | 219 | #log-box > span { 220 | border-top: 1px solid #3c3c3c; 221 | } 222 | 223 | #wviewer { 224 | grid-area: wviewer; 225 | display: flex; 226 | flex-direction: row; 227 | flex-wrap: wrap; 228 | align-items: center; 229 | justify-content: center; 230 | gap: 15px; 231 | } 232 | 233 | #wviewer::before { 234 | content: "Workers"; 235 | position: absolute; 236 | font-size: 13px; 237 | font-weight: bold; 238 | text-align: center; 239 | top: 0; 240 | left: 0; 241 | right: 0; 242 | } 243 | 244 | .worker { 245 | display: flex; 246 | flex-direction: column; 247 | align-items: center; 248 | justify-content: center; 249 | background: white; 250 | padding: 5px; 251 | border: 1px solid #3c3c3c; 252 | border-radius: 7px; 253 | } 254 | 255 | .worker span:first-child { 256 | font-weight: bold; 257 | margin-bottom: 5px; 258 | } 259 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rinzler demo 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 |
115 |
116 | 117 | 118 |
119 |
120 | 121 | 122 | 123 | 124 | no fake load 125 |
126 |
127 |
128 |
-no data-
129 |
130 |
131 |
132 |
133 |
134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | rinzler.js.org 2 | -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #24292F; 3 | --dark-hl-0: #C9D1D9; 4 | --light-hl-1: #CF222E; 5 | --dark-hl-1: #FF7B72; 6 | --light-hl-2: #0550AE; 7 | --dark-hl-2: #79C0FF; 8 | --light-hl-3: #0A3069; 9 | --dark-hl-3: #A5D6FF; 10 | --light-hl-4: #6E7781; 11 | --dark-hl-4: #8B949E; 12 | --light-hl-5: #8250DF; 13 | --dark-hl-5: #D2A8FF; 14 | --light-hl-6: #953800; 15 | --dark-hl-6: #FFA657; 16 | --light-code-background: #ffffff; 17 | --dark-code-background: #0d1117; 18 | } 19 | 20 | @media (prefers-color-scheme: light) { :root { 21 | --hl-0: var(--light-hl-0); 22 | --hl-1: var(--light-hl-1); 23 | --hl-2: var(--light-hl-2); 24 | --hl-3: var(--light-hl-3); 25 | --hl-4: var(--light-hl-4); 26 | --hl-5: var(--light-hl-5); 27 | --hl-6: var(--light-hl-6); 28 | --code-background: var(--light-code-background); 29 | } } 30 | 31 | @media (prefers-color-scheme: dark) { :root { 32 | --hl-0: var(--dark-hl-0); 33 | --hl-1: var(--dark-hl-1); 34 | --hl-2: var(--dark-hl-2); 35 | --hl-3: var(--dark-hl-3); 36 | --hl-4: var(--dark-hl-4); 37 | --hl-5: var(--dark-hl-5); 38 | --hl-6: var(--dark-hl-6); 39 | --code-background: var(--dark-code-background); 40 | } } 41 | 42 | :root[data-theme='light'] { 43 | --hl-0: var(--light-hl-0); 44 | --hl-1: var(--light-hl-1); 45 | --hl-2: var(--light-hl-2); 46 | --hl-3: var(--light-hl-3); 47 | --hl-4: var(--light-hl-4); 48 | --hl-5: var(--light-hl-5); 49 | --hl-6: var(--light-hl-6); 50 | --code-background: var(--light-code-background); 51 | } 52 | 53 | :root[data-theme='dark'] { 54 | --hl-0: var(--dark-hl-0); 55 | --hl-1: var(--dark-hl-1); 56 | --hl-2: var(--dark-hl-2); 57 | --hl-3: var(--dark-hl-3); 58 | --hl-4: var(--dark-hl-4); 59 | --hl-5: var(--dark-hl-5); 60 | --hl-6: var(--dark-hl-6); 61 | --code-background: var(--dark-code-background); 62 | } 63 | 64 | .hl-0 { color: var(--hl-0); } 65 | .hl-1 { color: var(--hl-1); } 66 | .hl-2 { color: var(--hl-2); } 67 | .hl-3 { color: var(--hl-3); } 68 | .hl-4 { color: var(--hl-4); } 69 | .hl-5 { color: var(--hl-5); } 70 | .hl-6 { color: var(--hl-6); } 71 | pre, code { background: var(--code-background); } 72 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"kinds\":{\"128\":\"Class\",\"512\":\"Constructor\",\"2048\":\"Method\",\"65536\":\"Type literal\",\"4194304\":\"Type alias\"},\"rows\":[{\"kind\":128,\"name\":\"RinzlerEngine\",\"url\":\"classes/RinzlerEngine.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/RinzlerEngine.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"RinzlerEngine\"},{\"kind\":2048,\"name\":\"configureAndStart\",\"url\":\"classes/RinzlerEngine.html#configureAndStart\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"RinzlerEngine\"},{\"kind\":2048,\"name\":\"runJob\",\"url\":\"classes/RinzlerEngine.html#runJob\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"RinzlerEngine\"},{\"kind\":2048,\"name\":\"shutdown\",\"url\":\"classes/RinzlerEngine.html#shutdown\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"RinzlerEngine\"},{\"kind\":2048,\"name\":\"preHeat\",\"url\":\"classes/RinzlerEngine.html#preHeat\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"RinzlerEngine\"},{\"kind\":2048,\"name\":\"afterburner\",\"url\":\"classes/RinzlerEngine.html#afterburner\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"RinzlerEngine\"},{\"kind\":2048,\"name\":\"_debug\",\"url\":\"classes/RinzlerEngine.html#_debug\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"RinzlerEngine\"},{\"kind\":4194304,\"name\":\"WorkerFunction\",\"url\":\"types/WorkerFunction.html\",\"classes\":\"tsd-kind-type-alias\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"types/WorkerFunction.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-alias\",\"parent\":\"WorkerFunction\"},{\"kind\":4194304,\"name\":\"WorkerInitFunction\",\"url\":\"types/WorkerInitFunction.html\",\"classes\":\"tsd-kind-type-alias\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"types/WorkerInitFunction.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-alias\",\"parent\":\"WorkerInitFunction\"},{\"kind\":4194304,\"name\":\"WorkerFunctionTransferArgs\",\"url\":\"types/WorkerFunctionTransferArgs.html\",\"classes\":\"tsd-kind-type-alias\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,22.336]],[\"comment/0\",[]],[\"name/1\",[1,22.336]],[\"comment/1\",[]],[\"name/2\",[2,22.336]],[\"comment/2\",[]],[\"name/3\",[3,22.336]],[\"comment/3\",[]],[\"name/4\",[4,22.336]],[\"comment/4\",[]],[\"name/5\",[5,22.336]],[\"comment/5\",[]],[\"name/6\",[6,22.336]],[\"comment/6\",[]],[\"name/7\",[7,22.336]],[\"comment/7\",[]],[\"name/8\",[8,22.336]],[\"comment/8\",[]],[\"name/9\",[9,17.228]],[\"comment/9\",[]],[\"name/10\",[10,22.336]],[\"comment/10\",[]],[\"name/11\",[9,17.228]],[\"comment/11\",[]],[\"name/12\",[11,22.336]],[\"comment/12\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":9,\"name\":{\"9\":{},\"11\":{}},\"comment\":{}}],[\"_debug\",{\"_index\":7,\"name\":{\"7\":{}},\"comment\":{}}],[\"afterburner\",{\"_index\":6,\"name\":{\"6\":{}},\"comment\":{}}],[\"configureandstart\",{\"_index\":2,\"name\":{\"2\":{}},\"comment\":{}}],[\"constructor\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"preheat\",{\"_index\":5,\"name\":{\"5\":{}},\"comment\":{}}],[\"rinzlerengine\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}],[\"runjob\",{\"_index\":3,\"name\":{\"3\":{}},\"comment\":{}}],[\"shutdown\",{\"_index\":4,\"name\":{\"4\":{}},\"comment\":{}}],[\"workerfunction\",{\"_index\":8,\"name\":{\"8\":{}},\"comment\":{}}],[\"workerfunctiontransferargs\",{\"_index\":11,\"name\":{\"12\":{}},\"comment\":{}}],[\"workerinitfunction\",{\"_index\":10,\"name\":{\"10\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Light */ 3 | --light-color-background: #f2f4f8; 4 | --light-color-background-secondary: #eff0f1; 5 | --light-color-icon-background: var(--light-color-background); 6 | --light-color-accent: #c5c7c9; 7 | --light-color-text: #222; 8 | --light-color-text-aside: #707070; 9 | --light-color-link: #4da6ff; 10 | --light-color-ts: #db1373; 11 | --light-color-ts-interface: #139d2c; 12 | --light-color-ts-enum: #9c891a; 13 | --light-color-ts-class: #2484e5; 14 | --light-color-ts-function: #572be7; 15 | --light-color-ts-namespace: #b111c9; 16 | --light-color-ts-private: #707070; 17 | --light-color-ts-variable: #4d68ff; 18 | --light-external-icon: url("data:image/svg+xml;utf8,"); 19 | --light-color-scheme: light; 20 | 21 | /* Dark */ 22 | --dark-color-background: #2b2e33; 23 | --dark-color-background-secondary: #1e2024; 24 | --dark-color-icon-background: var(--dark-color-background-secondary); 25 | --dark-color-accent: #9096a2; 26 | --dark-color-text: #f5f5f5; 27 | --dark-color-text-aside: #dddddd; 28 | --dark-color-link: #00aff4; 29 | --dark-color-ts: #ff6492; 30 | --dark-color-ts-interface: #6cff87; 31 | --dark-color-ts-enum: #f4d93e; 32 | --dark-color-ts-class: #61b0ff; 33 | --dark-color-ts-function: #9772ff; 34 | --dark-color-ts-namespace: #e14dff; 35 | --dark-color-ts-private: #e2e2e2; 36 | --dark-color-ts-variable: #4d68ff; 37 | --dark-external-icon: url("data:image/svg+xml;utf8,"); 38 | --dark-color-scheme: dark; 39 | } 40 | 41 | @media (prefers-color-scheme: light) { 42 | :root { 43 | --color-background: var(--light-color-background); 44 | --color-background-secondary: var(--light-color-background-secondary); 45 | --color-icon-background: var(--light-color-icon-background); 46 | --color-accent: var(--light-color-accent); 47 | --color-text: var(--light-color-text); 48 | --color-text-aside: var(--light-color-text-aside); 49 | --color-link: var(--light-color-link); 50 | --color-ts: var(--light-color-ts); 51 | --color-ts-interface: var(--light-color-ts-interface); 52 | --color-ts-enum: var(--light-color-ts-enum); 53 | --color-ts-class: var(--light-color-ts-class); 54 | --color-ts-function: var(--light-color-ts-function); 55 | --color-ts-namespace: var(--light-color-ts-namespace); 56 | --color-ts-private: var(--light-color-ts-private); 57 | --color-ts-variable: var(--light-color-ts-variable); 58 | --external-icon: var(--light-external-icon); 59 | --color-scheme: var(--light-color-scheme); 60 | } 61 | } 62 | 63 | @media (prefers-color-scheme: dark) { 64 | :root { 65 | --color-background: var(--dark-color-background); 66 | --color-background-secondary: var(--dark-color-background-secondary); 67 | --color-icon-background: var(--dark-color-icon-background); 68 | --color-accent: var(--dark-color-accent); 69 | --color-text: var(--dark-color-text); 70 | --color-text-aside: var(--dark-color-text-aside); 71 | --color-link: var(--dark-color-link); 72 | --color-ts: var(--dark-color-ts); 73 | --color-ts-interface: var(--dark-color-ts-interface); 74 | --color-ts-enum: var(--dark-color-ts-enum); 75 | --color-ts-class: var(--dark-color-ts-class); 76 | --color-ts-function: var(--dark-color-ts-function); 77 | --color-ts-namespace: var(--dark-color-ts-namespace); 78 | --color-ts-private: var(--dark-color-ts-private); 79 | --color-ts-variable: var(--dark-color-ts-variable); 80 | --external-icon: var(--dark-external-icon); 81 | --color-scheme: var(--dark-color-scheme); 82 | } 83 | } 84 | 85 | html { 86 | color-scheme: var(--color-scheme); 87 | } 88 | 89 | body { 90 | margin: 0; 91 | } 92 | 93 | :root[data-theme="light"] { 94 | --color-background: var(--light-color-background); 95 | --color-background-secondary: var(--light-color-background-secondary); 96 | --color-icon-background: var(--light-color-icon-background); 97 | --color-accent: var(--light-color-accent); 98 | --color-text: var(--light-color-text); 99 | --color-text-aside: var(--light-color-text-aside); 100 | --color-link: var(--light-color-link); 101 | --color-ts: var(--light-color-ts); 102 | --color-ts-interface: var(--light-color-ts-interface); 103 | --color-ts-enum: var(--light-color-ts-enum); 104 | --color-ts-class: var(--light-color-ts-class); 105 | --color-ts-function: var(--light-color-ts-function); 106 | --color-ts-namespace: var(--light-color-ts-namespace); 107 | --color-ts-private: var(--light-color-ts-private); 108 | --color-ts-variable: var(--light-color-ts-variable); 109 | --external-icon: var(--light-external-icon); 110 | --color-scheme: var(--light-color-scheme); 111 | } 112 | 113 | :root[data-theme="dark"] { 114 | --color-background: var(--dark-color-background); 115 | --color-background-secondary: var(--dark-color-background-secondary); 116 | --color-icon-background: var(--dark-color-icon-background); 117 | --color-accent: var(--dark-color-accent); 118 | --color-text: var(--dark-color-text); 119 | --color-text-aside: var(--dark-color-text-aside); 120 | --color-link: var(--dark-color-link); 121 | --color-ts: var(--dark-color-ts); 122 | --color-ts-interface: var(--dark-color-ts-interface); 123 | --color-ts-enum: var(--dark-color-ts-enum); 124 | --color-ts-class: var(--dark-color-ts-class); 125 | --color-ts-function: var(--dark-color-ts-function); 126 | --color-ts-namespace: var(--dark-color-ts-namespace); 127 | --color-ts-private: var(--dark-color-ts-private); 128 | --color-ts-variable: var(--dark-color-ts-variable); 129 | --external-icon: var(--dark-external-icon); 130 | --color-scheme: var(--dark-color-scheme); 131 | } 132 | 133 | h1, 134 | h2, 135 | h3, 136 | h4, 137 | h5, 138 | h6 { 139 | line-height: 1.2; 140 | } 141 | 142 | h1 { 143 | font-size: 1.875rem; 144 | margin: 0.67rem 0; 145 | } 146 | 147 | h2 { 148 | font-size: 1.5rem; 149 | margin: 0.83rem 0; 150 | } 151 | 152 | h3 { 153 | font-size: 1.25rem; 154 | margin: 1rem 0; 155 | } 156 | 157 | h4 { 158 | font-size: 1.05rem; 159 | margin: 1.33rem 0; 160 | } 161 | 162 | h5 { 163 | font-size: 1rem; 164 | margin: 1.5rem 0; 165 | } 166 | 167 | h6 { 168 | font-size: 0.875rem; 169 | margin: 2.33rem 0; 170 | } 171 | 172 | .uppercase { 173 | text-transform: uppercase; 174 | } 175 | 176 | pre { 177 | white-space: pre; 178 | white-space: pre-wrap; 179 | word-wrap: break-word; 180 | } 181 | 182 | dl, 183 | menu, 184 | ol, 185 | ul { 186 | margin: 1em 0; 187 | } 188 | 189 | dd { 190 | margin: 0 0 0 40px; 191 | } 192 | 193 | .container { 194 | max-width: 1600px; 195 | padding: 0 2rem; 196 | } 197 | 198 | @media (min-width: 640px) { 199 | .container { 200 | padding: 0 4rem; 201 | } 202 | } 203 | @media (min-width: 1200px) { 204 | .container { 205 | padding: 0 8rem; 206 | } 207 | } 208 | @media (min-width: 1600px) { 209 | .container { 210 | padding: 0 12rem; 211 | } 212 | } 213 | 214 | /* Footer */ 215 | .tsd-generator { 216 | border-top: 1px solid var(--color-accent); 217 | padding-top: 1rem; 218 | padding-bottom: 1rem; 219 | max-height: 3.5rem; 220 | } 221 | 222 | .tsd-generator > p { 223 | margin-top: 0; 224 | margin-bottom: 0; 225 | padding: 0 1rem; 226 | } 227 | 228 | .container-main { 229 | display: flex; 230 | justify-content: space-between; 231 | position: relative; 232 | margin: 0 auto; 233 | } 234 | 235 | .col-4, 236 | .col-8 { 237 | box-sizing: border-box; 238 | float: left; 239 | padding: 2rem 1rem; 240 | } 241 | 242 | .col-4 { 243 | flex: 0 0 25%; 244 | } 245 | .col-8 { 246 | flex: 1 0; 247 | flex-wrap: wrap; 248 | padding-left: 0; 249 | } 250 | 251 | @keyframes fade-in { 252 | from { 253 | opacity: 0; 254 | } 255 | to { 256 | opacity: 1; 257 | } 258 | } 259 | @keyframes fade-out { 260 | from { 261 | opacity: 1; 262 | visibility: visible; 263 | } 264 | to { 265 | opacity: 0; 266 | } 267 | } 268 | @keyframes fade-in-delayed { 269 | 0% { 270 | opacity: 0; 271 | } 272 | 33% { 273 | opacity: 0; 274 | } 275 | 100% { 276 | opacity: 1; 277 | } 278 | } 279 | @keyframes fade-out-delayed { 280 | 0% { 281 | opacity: 1; 282 | visibility: visible; 283 | } 284 | 66% { 285 | opacity: 0; 286 | } 287 | 100% { 288 | opacity: 0; 289 | } 290 | } 291 | @keyframes shift-to-left { 292 | from { 293 | transform: translate(0, 0); 294 | } 295 | to { 296 | transform: translate(-25%, 0); 297 | } 298 | } 299 | @keyframes unshift-to-left { 300 | from { 301 | transform: translate(-25%, 0); 302 | } 303 | to { 304 | transform: translate(0, 0); 305 | } 306 | } 307 | @keyframes pop-in-from-right { 308 | from { 309 | transform: translate(100%, 0); 310 | } 311 | to { 312 | transform: translate(0, 0); 313 | } 314 | } 315 | @keyframes pop-out-to-right { 316 | from { 317 | transform: translate(0, 0); 318 | visibility: visible; 319 | } 320 | to { 321 | transform: translate(100%, 0); 322 | } 323 | } 324 | body { 325 | background: var(--color-background); 326 | font-family: "Segoe UI", sans-serif; 327 | font-size: 16px; 328 | color: var(--color-text); 329 | } 330 | 331 | a { 332 | color: var(--color-link); 333 | text-decoration: none; 334 | } 335 | a:hover { 336 | text-decoration: underline; 337 | } 338 | a.external[target="_blank"] { 339 | background-image: var(--external-icon); 340 | background-position: top 3px right; 341 | background-repeat: no-repeat; 342 | padding-right: 13px; 343 | } 344 | 345 | code, 346 | pre { 347 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 348 | padding: 0.2em; 349 | margin: 0; 350 | font-size: 0.875rem; 351 | border-radius: 0.8em; 352 | } 353 | 354 | pre { 355 | padding: 10px; 356 | border: 0.1em solid var(--color-accent); 357 | } 358 | pre code { 359 | padding: 0; 360 | font-size: 100%; 361 | } 362 | 363 | blockquote { 364 | margin: 1em 0; 365 | padding-left: 1em; 366 | border-left: 4px solid gray; 367 | } 368 | 369 | .tsd-typography { 370 | line-height: 1.333em; 371 | } 372 | .tsd-typography ul { 373 | list-style: square; 374 | padding: 0 0 0 20px; 375 | margin: 0; 376 | } 377 | .tsd-typography h4, 378 | .tsd-typography .tsd-index-panel h3, 379 | .tsd-index-panel .tsd-typography h3, 380 | .tsd-typography h5, 381 | .tsd-typography h6 { 382 | font-size: 1em; 383 | margin: 0; 384 | } 385 | .tsd-typography h5, 386 | .tsd-typography h6 { 387 | font-weight: normal; 388 | } 389 | .tsd-typography p, 390 | .tsd-typography ul, 391 | .tsd-typography ol { 392 | margin: 1em 0; 393 | } 394 | 395 | @media (max-width: 1024px) { 396 | html .col-content { 397 | float: none; 398 | max-width: 100%; 399 | width: 100%; 400 | padding-top: 3rem; 401 | } 402 | html .col-menu { 403 | position: fixed !important; 404 | overflow-y: auto; 405 | -webkit-overflow-scrolling: touch; 406 | z-index: 1024; 407 | top: 0 !important; 408 | bottom: 0 !important; 409 | left: auto !important; 410 | right: 0 !important; 411 | padding: 1.5rem 1.5rem 0 0; 412 | max-width: 25rem; 413 | visibility: hidden; 414 | background-color: var(--color-background); 415 | transform: translate(100%, 0); 416 | } 417 | html .col-menu > *:last-child { 418 | padding-bottom: 20px; 419 | } 420 | html .overlay { 421 | content: ""; 422 | display: block; 423 | position: fixed; 424 | z-index: 1023; 425 | top: 0; 426 | left: 0; 427 | right: 0; 428 | bottom: 0; 429 | background-color: rgba(0, 0, 0, 0.75); 430 | visibility: hidden; 431 | } 432 | 433 | .to-has-menu .overlay { 434 | animation: fade-in 0.4s; 435 | } 436 | 437 | .to-has-menu :is(header, footer, .col-content) { 438 | animation: shift-to-left 0.4s; 439 | } 440 | 441 | .to-has-menu .col-menu { 442 | animation: pop-in-from-right 0.4s; 443 | } 444 | 445 | .from-has-menu .overlay { 446 | animation: fade-out 0.4s; 447 | } 448 | 449 | .from-has-menu :is(header, footer, .col-content) { 450 | animation: unshift-to-left 0.4s; 451 | } 452 | 453 | .from-has-menu .col-menu { 454 | animation: pop-out-to-right 0.4s; 455 | } 456 | 457 | .has-menu body { 458 | overflow: hidden; 459 | } 460 | .has-menu .overlay { 461 | visibility: visible; 462 | } 463 | .has-menu :is(header, footer, .col-content) { 464 | transform: translate(-25%, 0); 465 | } 466 | .has-menu .col-menu { 467 | visibility: visible; 468 | transform: translate(0, 0); 469 | display: grid; 470 | align-items: center; 471 | grid-template-rows: auto 1fr; 472 | grid-gap: 1.5rem; 473 | max-height: 100vh; 474 | padding: 1rem 2rem; 475 | } 476 | .has-menu .tsd-navigation { 477 | max-height: 100%; 478 | } 479 | } 480 | 481 | .tsd-breadcrumb { 482 | margin: 0; 483 | padding: 0; 484 | color: var(--color-text-aside); 485 | } 486 | .tsd-breadcrumb a { 487 | color: var(--color-text-aside); 488 | text-decoration: none; 489 | } 490 | .tsd-breadcrumb a:hover { 491 | text-decoration: underline; 492 | } 493 | .tsd-breadcrumb li { 494 | display: inline; 495 | } 496 | .tsd-breadcrumb li:after { 497 | content: " / "; 498 | } 499 | 500 | .tsd-comment-tags { 501 | display: flex; 502 | flex-direction: column; 503 | } 504 | dl.tsd-comment-tag-group { 505 | display: flex; 506 | align-items: center; 507 | overflow: hidden; 508 | margin: 0.5em 0; 509 | } 510 | dl.tsd-comment-tag-group dt { 511 | display: flex; 512 | margin-right: 0.5em; 513 | font-size: 0.875em; 514 | font-weight: normal; 515 | } 516 | dl.tsd-comment-tag-group dd { 517 | margin: 0; 518 | } 519 | code.tsd-tag { 520 | padding: 0.25em 0.4em; 521 | border: 0.1em solid var(--color-accent); 522 | margin-right: 0.25em; 523 | font-size: 70%; 524 | } 525 | h1 code.tsd-tag:first-of-type { 526 | margin-left: 0.25em; 527 | } 528 | 529 | dl.tsd-comment-tag-group dd:before, 530 | dl.tsd-comment-tag-group dd:after { 531 | content: " "; 532 | } 533 | dl.tsd-comment-tag-group dd pre, 534 | dl.tsd-comment-tag-group dd:after { 535 | clear: both; 536 | } 537 | dl.tsd-comment-tag-group p { 538 | margin: 0; 539 | } 540 | 541 | .tsd-panel.tsd-comment .lead { 542 | font-size: 1.1em; 543 | line-height: 1.333em; 544 | margin-bottom: 2em; 545 | } 546 | .tsd-panel.tsd-comment .lead:last-child { 547 | margin-bottom: 0; 548 | } 549 | 550 | .tsd-filter-visibility h4 { 551 | font-size: 1rem; 552 | padding-top: 0.75rem; 553 | padding-bottom: 0.5rem; 554 | margin: 0; 555 | } 556 | .tsd-filter-item:not(:last-child) { 557 | margin-bottom: 0.5rem; 558 | } 559 | .tsd-filter-input { 560 | display: flex; 561 | width: fit-content; 562 | width: -moz-fit-content; 563 | align-items: center; 564 | user-select: none; 565 | -webkit-user-select: none; 566 | -moz-user-select: none; 567 | -ms-user-select: none; 568 | cursor: pointer; 569 | } 570 | .tsd-filter-input input[type="checkbox"] { 571 | cursor: pointer; 572 | position: absolute; 573 | width: 1.5em; 574 | height: 1.5em; 575 | opacity: 0; 576 | } 577 | .tsd-filter-input input[type="checkbox"]:disabled { 578 | pointer-events: none; 579 | } 580 | .tsd-filter-input svg { 581 | cursor: pointer; 582 | width: 1.5em; 583 | height: 1.5em; 584 | margin-right: 0.5em; 585 | border-radius: 0.33em; 586 | /* Leaving this at full opacity breaks event listeners on Firefox. 587 | Don't remove unless you know what you're doing. */ 588 | opacity: 0.99; 589 | } 590 | .tsd-filter-input input[type="checkbox"]:focus + svg { 591 | transform: scale(0.95); 592 | } 593 | .tsd-filter-input input[type="checkbox"]:focus:not(:focus-visible) + svg { 594 | transform: scale(1); 595 | } 596 | .tsd-checkbox-background { 597 | fill: var(--color-accent); 598 | } 599 | input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { 600 | stroke: var(--color-text); 601 | } 602 | .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { 603 | fill: var(--color-background); 604 | stroke: var(--color-accent); 605 | stroke-width: 0.25rem; 606 | } 607 | .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { 608 | stroke: var(--color-accent); 609 | } 610 | 611 | .tsd-theme-toggle { 612 | padding-top: 0.75rem; 613 | } 614 | .tsd-theme-toggle > h4 { 615 | display: inline; 616 | vertical-align: middle; 617 | margin-right: 0.75rem; 618 | } 619 | 620 | .tsd-hierarchy { 621 | list-style: square; 622 | margin: 0; 623 | } 624 | .tsd-hierarchy .target { 625 | font-weight: bold; 626 | } 627 | 628 | .tsd-panel-group.tsd-index-group { 629 | margin-bottom: 0; 630 | } 631 | .tsd-index-panel .tsd-index-list { 632 | list-style: none; 633 | line-height: 1.333em; 634 | margin: 0; 635 | padding: 0.25rem 0 0 0; 636 | overflow: hidden; 637 | display: grid; 638 | grid-template-columns: repeat(3, 1fr); 639 | column-gap: 1rem; 640 | grid-template-rows: auto; 641 | } 642 | @media (max-width: 1024px) { 643 | .tsd-index-panel .tsd-index-list { 644 | grid-template-columns: repeat(2, 1fr); 645 | } 646 | } 647 | @media (max-width: 768px) { 648 | .tsd-index-panel .tsd-index-list { 649 | grid-template-columns: repeat(1, 1fr); 650 | } 651 | } 652 | .tsd-index-panel .tsd-index-list li { 653 | -webkit-page-break-inside: avoid; 654 | -moz-page-break-inside: avoid; 655 | -ms-page-break-inside: avoid; 656 | -o-page-break-inside: avoid; 657 | page-break-inside: avoid; 658 | } 659 | .tsd-index-panel a, 660 | .tsd-index-panel a.tsd-parent-kind-module { 661 | color: var(--color-ts); 662 | } 663 | .tsd-index-panel a.tsd-parent-kind-interface { 664 | color: var(--color-ts-interface); 665 | } 666 | .tsd-index-panel a.tsd-parent-kind-enum { 667 | color: var(--color-ts-enum); 668 | } 669 | .tsd-index-panel a.tsd-parent-kind-class { 670 | color: var(--color-ts-class); 671 | } 672 | .tsd-index-panel a.tsd-kind-module { 673 | color: var(--color-ts-namespace); 674 | } 675 | .tsd-index-panel a.tsd-kind-interface { 676 | color: var(--color-ts-interface); 677 | } 678 | .tsd-index-panel a.tsd-kind-enum { 679 | color: var(--color-ts-enum); 680 | } 681 | .tsd-index-panel a.tsd-kind-class { 682 | color: var(--color-ts-class); 683 | } 684 | .tsd-index-panel a.tsd-kind-function { 685 | color: var(--color-ts-function); 686 | } 687 | .tsd-index-panel a.tsd-kind-namespace { 688 | color: var(--color-ts-namespace); 689 | } 690 | .tsd-index-panel a.tsd-kind-variable { 691 | color: var(--color-ts-variable); 692 | } 693 | .tsd-index-panel a.tsd-is-private { 694 | color: var(--color-ts-private); 695 | } 696 | 697 | .tsd-flag { 698 | display: inline-block; 699 | padding: 0.25em 0.4em; 700 | border-radius: 4px; 701 | color: var(--color-comment-tag-text); 702 | background-color: var(--color-comment-tag); 703 | text-indent: 0; 704 | font-size: 75%; 705 | line-height: 1; 706 | font-weight: normal; 707 | } 708 | 709 | .tsd-anchor { 710 | position: absolute; 711 | top: -100px; 712 | } 713 | 714 | .tsd-member { 715 | position: relative; 716 | } 717 | .tsd-member .tsd-anchor + h3 { 718 | display: flex; 719 | align-items: center; 720 | margin-top: 0; 721 | margin-bottom: 0; 722 | border-bottom: none; 723 | } 724 | .tsd-member [data-tsd-kind] { 725 | color: var(--color-ts); 726 | } 727 | .tsd-member [data-tsd-kind="Interface"] { 728 | color: var(--color-ts-interface); 729 | } 730 | .tsd-member [data-tsd-kind="Enum"] { 731 | color: var(--color-ts-enum); 732 | } 733 | .tsd-member [data-tsd-kind="Class"] { 734 | color: var(--color-ts-class); 735 | } 736 | .tsd-member [data-tsd-kind="Private"] { 737 | color: var(--color-ts-private); 738 | } 739 | 740 | .tsd-navigation a { 741 | display: block; 742 | margin: 0.4rem 0; 743 | border-left: 2px solid transparent; 744 | color: var(--color-text); 745 | text-decoration: none; 746 | transition: border-left-color 0.1s; 747 | } 748 | .tsd-navigation a:hover { 749 | text-decoration: underline; 750 | } 751 | .tsd-navigation ul { 752 | margin: 0; 753 | padding: 0; 754 | list-style: none; 755 | } 756 | .tsd-navigation li { 757 | padding: 0; 758 | } 759 | 760 | .tsd-navigation.primary .tsd-accordion-details > ul { 761 | margin-top: 0.75rem; 762 | } 763 | .tsd-navigation.primary a { 764 | padding: 0.75rem 0.5rem; 765 | margin: 0; 766 | } 767 | .tsd-navigation.primary ul li a { 768 | margin-left: 0.5rem; 769 | } 770 | .tsd-navigation.primary ul li li a { 771 | margin-left: 1.5rem; 772 | } 773 | .tsd-navigation.primary ul li li li a { 774 | margin-left: 2.5rem; 775 | } 776 | .tsd-navigation.primary ul li li li li a { 777 | margin-left: 3.5rem; 778 | } 779 | .tsd-navigation.primary ul li li li li li a { 780 | margin-left: 4.5rem; 781 | } 782 | .tsd-navigation.primary ul li li li li li li a { 783 | margin-left: 5.5rem; 784 | } 785 | .tsd-navigation.primary li.current > a { 786 | border-left: 0.15rem var(--color-text) solid; 787 | } 788 | .tsd-navigation.primary li.selected > a { 789 | font-weight: bold; 790 | border-left: 0.2rem var(--color-text) solid; 791 | } 792 | .tsd-navigation.primary ul li a:hover { 793 | border-left: 0.2rem var(--color-text-aside) solid; 794 | } 795 | .tsd-navigation.primary li.globals + li > span, 796 | .tsd-navigation.primary li.globals + li > a { 797 | padding-top: 20px; 798 | } 799 | 800 | .tsd-navigation.secondary.tsd-navigation--toolbar-hide { 801 | max-height: calc(100vh - 1rem); 802 | top: 0.5rem; 803 | } 804 | .tsd-navigation.secondary > ul { 805 | display: inline; 806 | padding-right: 0.5rem; 807 | transition: opacity 0.2s; 808 | } 809 | .tsd-navigation.secondary ul li a { 810 | padding-left: 0; 811 | } 812 | .tsd-navigation.secondary ul li li a { 813 | padding-left: 1.1rem; 814 | } 815 | .tsd-navigation.secondary ul li li li a { 816 | padding-left: 2.2rem; 817 | } 818 | .tsd-navigation.secondary ul li li li li a { 819 | padding-left: 3.3rem; 820 | } 821 | .tsd-navigation.secondary ul li li li li li a { 822 | padding-left: 4.4rem; 823 | } 824 | .tsd-navigation.secondary ul li li li li li li a { 825 | padding-left: 5.5rem; 826 | } 827 | 828 | #tsd-sidebar-links a { 829 | margin-top: 0; 830 | margin-bottom: 0.5rem; 831 | line-height: 1.25rem; 832 | } 833 | #tsd-sidebar-links a:last-of-type { 834 | margin-bottom: 0; 835 | } 836 | 837 | a.tsd-index-link { 838 | margin: 0.25rem 0; 839 | font-size: 1rem; 840 | line-height: 1.25rem; 841 | display: inline-flex; 842 | align-items: center; 843 | } 844 | .tsd-accordion-summary > h1, 845 | .tsd-accordion-summary > h2, 846 | .tsd-accordion-summary > h3, 847 | .tsd-accordion-summary > h4, 848 | .tsd-accordion-summary > h5 { 849 | display: inline-flex; 850 | align-items: center; 851 | vertical-align: middle; 852 | margin-bottom: 0; 853 | user-select: none; 854 | -moz-user-select: none; 855 | -webkit-user-select: none; 856 | -ms-user-select: none; 857 | } 858 | .tsd-accordion-summary { 859 | display: block; 860 | cursor: pointer; 861 | } 862 | .tsd-accordion-summary > * { 863 | margin-top: 0; 864 | margin-bottom: 0; 865 | padding-top: 0; 866 | padding-bottom: 0; 867 | } 868 | .tsd-accordion-summary::-webkit-details-marker { 869 | display: none; 870 | } 871 | .tsd-index-accordion .tsd-accordion-summary svg { 872 | margin-right: 0.25rem; 873 | } 874 | .tsd-index-content > :not(:first-child) { 875 | margin-top: 0.75rem; 876 | } 877 | .tsd-index-heading { 878 | margin-top: 1.5rem; 879 | margin-bottom: 0.75rem; 880 | } 881 | 882 | .tsd-kind-icon { 883 | margin-right: 0.5rem; 884 | width: 1.25rem; 885 | height: 1.25rem; 886 | min-width: 1.25rem; 887 | min-height: 1.25rem; 888 | } 889 | .tsd-kind-icon path { 890 | transform-origin: center; 891 | transform: scale(1.1); 892 | } 893 | .tsd-signature > .tsd-kind-icon { 894 | margin-right: 0.8rem; 895 | } 896 | 897 | @media (min-width: 1024px) { 898 | .col-content { 899 | margin: 2rem auto; 900 | } 901 | 902 | .menu-sticky-wrap { 903 | position: sticky; 904 | height: calc(100vh - 2rem); 905 | top: 4rem; 906 | right: 0; 907 | padding: 0 1.5rem; 908 | padding-top: 1rem; 909 | margin-top: 3rem; 910 | transition: 0.3s ease-in-out; 911 | transition-property: top, padding-top, padding, height; 912 | overflow-y: auto; 913 | } 914 | .col-menu { 915 | border-left: 1px solid var(--color-accent); 916 | } 917 | .col-menu--hide { 918 | top: 1rem; 919 | } 920 | .col-menu .tsd-navigation:not(:last-child) { 921 | padding-bottom: 1.75rem; 922 | } 923 | } 924 | 925 | .tsd-panel { 926 | margin-bottom: 2.5rem; 927 | } 928 | .tsd-panel.tsd-member { 929 | margin-bottom: 4rem; 930 | } 931 | .tsd-panel:empty { 932 | display: none; 933 | } 934 | .tsd-panel > h1, 935 | .tsd-panel > h2, 936 | .tsd-panel > h3 { 937 | margin: 1.5rem -1.5rem 0.75rem -1.5rem; 938 | padding: 0 1.5rem 0.75rem 1.5rem; 939 | } 940 | .tsd-panel > h1.tsd-before-signature, 941 | .tsd-panel > h2.tsd-before-signature, 942 | .tsd-panel > h3.tsd-before-signature { 943 | margin-bottom: 0; 944 | border-bottom: none; 945 | } 946 | 947 | .tsd-panel-group { 948 | margin: 4rem 0; 949 | } 950 | .tsd-panel-group.tsd-index-group { 951 | margin: 2rem 0; 952 | } 953 | .tsd-panel-group.tsd-index-group details { 954 | margin: 2rem 0; 955 | } 956 | 957 | #tsd-search { 958 | transition: background-color 0.2s; 959 | } 960 | #tsd-search .title { 961 | position: relative; 962 | z-index: 2; 963 | } 964 | #tsd-search .field { 965 | position: absolute; 966 | left: 0; 967 | top: 0; 968 | right: 2.5rem; 969 | height: 100%; 970 | } 971 | #tsd-search .field input { 972 | box-sizing: border-box; 973 | position: relative; 974 | top: -50px; 975 | z-index: 1; 976 | width: 100%; 977 | padding: 0 10px; 978 | opacity: 0; 979 | outline: 0; 980 | border: 0; 981 | background: transparent; 982 | color: var(--color-text); 983 | } 984 | #tsd-search .field label { 985 | position: absolute; 986 | overflow: hidden; 987 | right: -40px; 988 | } 989 | #tsd-search .field input, 990 | #tsd-search .title, 991 | #tsd-toolbar-links a { 992 | transition: opacity 0.2s; 993 | } 994 | #tsd-search .results { 995 | position: absolute; 996 | visibility: hidden; 997 | top: 40px; 998 | width: 100%; 999 | margin: 0; 1000 | padding: 0; 1001 | list-style: none; 1002 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); 1003 | } 1004 | #tsd-search .results li { 1005 | padding: 0 10px; 1006 | background-color: var(--color-background); 1007 | } 1008 | #tsd-search .results li:nth-child(even) { 1009 | background-color: var(--color-background-secondary); 1010 | } 1011 | #tsd-search .results li.state { 1012 | display: none; 1013 | } 1014 | #tsd-search .results li.current, 1015 | #tsd-search .results li:hover { 1016 | background-color: var(--color-accent); 1017 | } 1018 | #tsd-search .results a { 1019 | display: block; 1020 | } 1021 | #tsd-search .results a:before { 1022 | top: 10px; 1023 | } 1024 | #tsd-search .results span.parent { 1025 | color: var(--color-text-aside); 1026 | font-weight: normal; 1027 | } 1028 | #tsd-search.has-focus { 1029 | background-color: var(--color-accent); 1030 | } 1031 | #tsd-search.has-focus .field input { 1032 | top: 0; 1033 | opacity: 1; 1034 | } 1035 | #tsd-search.has-focus .title, 1036 | #tsd-search.has-focus #tsd-toolbar-links a { 1037 | z-index: 0; 1038 | opacity: 0; 1039 | } 1040 | #tsd-search.has-focus .results { 1041 | visibility: visible; 1042 | } 1043 | #tsd-search.loading .results li.state.loading { 1044 | display: block; 1045 | } 1046 | #tsd-search.failure .results li.state.failure { 1047 | display: block; 1048 | } 1049 | 1050 | #tsd-toolbar-links { 1051 | position: absolute; 1052 | top: 0; 1053 | right: 2rem; 1054 | height: 100%; 1055 | display: flex; 1056 | align-items: center; 1057 | justify-content: flex-end; 1058 | } 1059 | #tsd-toolbar-links a { 1060 | margin-left: 1.5rem; 1061 | } 1062 | #tsd-toolbar-links a:hover { 1063 | text-decoration: underline; 1064 | } 1065 | 1066 | .tsd-signature { 1067 | margin: 0 0 1rem 0; 1068 | padding: 1rem 0.5rem; 1069 | border: 1px solid var(--color-accent); 1070 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 1071 | font-size: 14px; 1072 | overflow-x: auto; 1073 | } 1074 | 1075 | .tsd-signature-symbol { 1076 | color: var(--color-text-aside); 1077 | font-weight: normal; 1078 | } 1079 | 1080 | .tsd-signature-type { 1081 | font-style: italic; 1082 | font-weight: normal; 1083 | } 1084 | 1085 | .tsd-signatures { 1086 | padding: 0; 1087 | margin: 0 0 1em 0; 1088 | list-style-type: none; 1089 | } 1090 | .tsd-signatures .tsd-signature { 1091 | margin: 0; 1092 | border-color: var(--color-accent); 1093 | border-width: 1px 0; 1094 | transition: background-color 0.1s; 1095 | } 1096 | .tsd-description .tsd-signatures .tsd-signature { 1097 | border-width: 1px; 1098 | } 1099 | 1100 | ul.tsd-parameter-list, 1101 | ul.tsd-type-parameter-list { 1102 | list-style: square; 1103 | margin: 0; 1104 | padding-left: 20px; 1105 | } 1106 | ul.tsd-parameter-list > li.tsd-parameter-signature, 1107 | ul.tsd-type-parameter-list > li.tsd-parameter-signature { 1108 | list-style: none; 1109 | margin-left: -20px; 1110 | } 1111 | ul.tsd-parameter-list h5, 1112 | ul.tsd-type-parameter-list h5 { 1113 | font-size: 16px; 1114 | margin: 1em 0 0.5em 0; 1115 | } 1116 | .tsd-sources { 1117 | margin-top: 1rem; 1118 | font-size: 0.875em; 1119 | } 1120 | .tsd-sources a { 1121 | color: var(--color-text-aside); 1122 | text-decoration: underline; 1123 | } 1124 | .tsd-sources ul { 1125 | list-style: none; 1126 | padding: 0; 1127 | } 1128 | 1129 | .tsd-page-toolbar { 1130 | position: fixed; 1131 | z-index: 1; 1132 | top: 0; 1133 | left: 0; 1134 | width: 100%; 1135 | color: var(--color-text); 1136 | background: var(--color-background-secondary); 1137 | border-bottom: 1px var(--color-accent) solid; 1138 | transition: transform 0.3s ease-in-out; 1139 | } 1140 | .tsd-page-toolbar a { 1141 | color: var(--color-text); 1142 | text-decoration: none; 1143 | } 1144 | .tsd-page-toolbar a.title { 1145 | font-weight: bold; 1146 | } 1147 | .tsd-page-toolbar a.title:hover { 1148 | text-decoration: underline; 1149 | } 1150 | .tsd-page-toolbar .tsd-toolbar-contents { 1151 | display: flex; 1152 | justify-content: space-between; 1153 | height: 2.5rem; 1154 | margin: 0 auto; 1155 | } 1156 | .tsd-page-toolbar .table-cell { 1157 | position: relative; 1158 | white-space: nowrap; 1159 | line-height: 40px; 1160 | } 1161 | .tsd-page-toolbar .table-cell:first-child { 1162 | width: 100%; 1163 | } 1164 | .tsd-page-toolbar .tsd-toolbar-icon { 1165 | box-sizing: border-box; 1166 | line-height: 0; 1167 | padding: 12px 0; 1168 | } 1169 | 1170 | .tsd-page-toolbar--hide { 1171 | transform: translateY(-100%); 1172 | } 1173 | 1174 | .tsd-widget { 1175 | display: inline-block; 1176 | overflow: hidden; 1177 | opacity: 0.8; 1178 | height: 40px; 1179 | transition: opacity 0.1s, background-color 0.2s; 1180 | vertical-align: bottom; 1181 | cursor: pointer; 1182 | } 1183 | .tsd-widget:hover { 1184 | opacity: 0.9; 1185 | } 1186 | .tsd-widget.active { 1187 | opacity: 1; 1188 | background-color: var(--color-accent); 1189 | } 1190 | .tsd-widget.no-caption { 1191 | width: 40px; 1192 | } 1193 | .tsd-widget.no-caption:before { 1194 | margin: 0; 1195 | } 1196 | 1197 | .tsd-widget.options, 1198 | .tsd-widget.menu { 1199 | display: none; 1200 | } 1201 | @media (max-width: 1024px) { 1202 | .tsd-widget.options, 1203 | .tsd-widget.menu { 1204 | display: inline-block; 1205 | } 1206 | } 1207 | input[type="checkbox"] + .tsd-widget:before { 1208 | background-position: -120px 0; 1209 | } 1210 | input[type="checkbox"]:checked + .tsd-widget:before { 1211 | background-position: -160px 0; 1212 | } 1213 | 1214 | img { 1215 | max-width: 100%; 1216 | } 1217 | 1218 | .tsd-anchor-icon { 1219 | display: inline-flex; 1220 | align-items: center; 1221 | margin-left: 0.5rem; 1222 | vertical-align: middle; 1223 | color: var(--color-text); 1224 | } 1225 | 1226 | .tsd-anchor-icon svg { 1227 | width: 1em; 1228 | height: 1em; 1229 | visibility: hidden; 1230 | } 1231 | 1232 | .tsd-anchor-link:hover > .tsd-anchor-icon svg { 1233 | visibility: visible; 1234 | } 1235 | 1236 | .deprecated { 1237 | text-decoration: line-through; 1238 | } 1239 | 1240 | * { 1241 | scrollbar-width: thin; 1242 | scrollbar-color: var(--color-accent) var(--color-icon-background); 1243 | } 1244 | 1245 | *::-webkit-scrollbar { 1246 | width: 0.75rem; 1247 | } 1248 | 1249 | *::-webkit-scrollbar-track { 1250 | background: var(--color-icon-background); 1251 | } 1252 | 1253 | *::-webkit-scrollbar-thumb { 1254 | background-color: var(--color-accent); 1255 | border-radius: 999rem; 1256 | border: 0.25rem solid var(--color-icon-background); 1257 | } 1258 | -------------------------------------------------------------------------------- /docs/classes/RinzlerEngine.html: -------------------------------------------------------------------------------- 1 | RinzlerEngine | Rinzler - v1.1.2
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Class RinzlerEngine

18 |
19 |
20 | 21 |

Welcome to Rinzler's full documentation.

22 |
23 |

If you're just getting started, check out the Quick Start guide.

24 |

This page describes the interface of an instanted RinzlerEngine class, which you can access like so:

25 |
import RinzlerEngine from 'rinzler-engine'

const engine = new RinzlerEngine() 26 |
27 |
28 |
29 |

Hierarchy

30 |
    31 |
  • RinzlerEngine
34 |
35 |
36 |
37 | 38 |
39 |
40 |

Constructors

41 |
43 |
44 |

Methods - Lifecycle

45 |
49 |
50 |

Methods - Optimization

51 |
preHeat 52 |
53 |
54 |

Methods - Other

55 |
_debug 56 |
57 |
58 |

Methods - ⚠️ Dangerous

59 |
61 |
62 |

Constructors

63 |
64 | 65 |
69 |
70 |

Lifecycle Methods

71 |
72 | 73 |
    74 | 75 |
  • 76 |

    Start the engine.

    77 |

    Internally, configures the job processing functions, starts the load balancer and launches a first Web Worker.

    78 | 79 |

    Returns

    A Promise for the current engine instance. Can be used for chaining multiple instance method calls.

    80 |
    81 |
    82 |

    Parameters

    83 |
      84 |
    • 85 |
      workFunction: WorkerFunction<unknown>
      86 |

      The function which will process all jobs sent to this engine instance.

      87 |
    • 88 |
    • 89 |
      Optional initFunction: WorkerInitFunction<unknown>
      90 |

      A function for setting up a Web Worker environment before it starts processing jobs. May be sync or async.

      91 |
    • 92 |
    • 93 |
      Optional initArgs: WorkerFunctionTransferArgs
      94 |

      Dynamic data to send to the initFunction to set up the environment of new Web Worker instances.

      95 |
    96 |

    Returns Promise<RinzlerEngine>

99 |
100 | 101 |
    102 | 103 |
  • 104 |

    Schedule, execute and get the results of a job.

    105 | 106 |

    Returns

    A Promise that will be fulfilled with T or an empty resolve when the job has been processed.

    107 |
    If the job threw an error when processing, the Promise will reject with an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Instance_properties) object containing the job's error message.
    108 | 
    109 |
    110 |
    111 |

    Type Parameters

    112 |
      113 |
    • 114 |

      T = void

      115 |

      The return type of a successful job - meaning what the WorkerFunction will return, with Transferable objects inlined.

      116 |
    117 |
    118 |

    Parameters

    119 |
    122 |

    Returns Promise<T>

125 |
126 | 127 |
    128 | 129 |
  • 130 |

    Stop the engine.

    131 |

    Wait for any remaining tasks to complete, shuts down all WebWorkers and sets the engine instance back to a blank state.

    132 |
    133 |

    Returns Promise<void>

136 |
137 |

Optimization Methods

138 |
139 | 140 |
    141 | 142 |
  • 143 |

    Start as much WebWorkers as currently recommended by the browser, or allowed by afterburner.

    144 |

    You should use this only if you know you will have to process many jobs and want to ensure that the engine is fully revved up. 145 | In most cases the automatic "temperature" adjustement will provide a good balance between performance and memory footprint.

    146 |

    You should immediatly follow this call with your actual runJob calls, otherwise the engine might start to cool itself down automatically.

    147 | 148 |

    Returns

    A Promise for the current engine instance. Can be used for chaining multiple instance method calls.

    149 |
    150 |

    Returns Promise<RinzlerEngine>

153 |
154 |

Other Methods

155 |
156 | 157 |
    158 | 159 |
  • 160 |

    Returns Record<string, unknown>

163 |
164 |

⚠️ Dangerous Methods

165 |
166 | 167 |
    168 | 169 |
  • 170 |

    WARNING: Possibly unstable.

    171 |

    Overrides the maximum number of WebWorkers Rinzler will attempt to spawn.

    172 |

    Normally this is retrieved from navigator.hardwareConcurrency, which is a browser-provided indicator of how many Workers a browsing context should use, with respect to the browser's own limitations and the number of available CPU cores on the host hardware.

    173 |

    By going beyond that number you may crash the browser tab, get unexpected errors, or a considerable boost in parallelized performance. 174 | Aim for the moon, right?

    175 | 176 |

    Returns

    A Promise for the current engine instance. Can be used for chaining multiple instance method calls.

    177 |
    178 |
    179 |

    Parameters

    180 |
      181 |
    • 182 |
      max: number
      183 |

      The new maximum number of WebWorkers this instance will manage. If set to lower than the current max, extra Workers will be gracefully shut down to respect the new limit.

      184 |
    185 |

    Returns Promise<RinzlerEngine>

188 |
217 |
218 |

Generated using TypeDoc

219 |
-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Rinzler - v1.1.2
2 |
3 | 10 |
11 |
12 |
13 |
14 |

Rinzler - v1.1.2

15 |
16 | Rinzler project logo 17 |

18 | npm version badge 19 | types included badge 20 | license badge 21 |


22 |
23 | 24 |

Rinzler is a turboramjet parallel processing engine for the browser.

25 |

It speeds up your web application by allowing recurring functions to execute in parallel taking full advantage of the host system's available processing power.

26 |

Check out the full docs, try the interactive demo or read on for a high-level overview and a quick start guide.

27 | 28 | 29 |

Concept

30 |
31 |

Most devices have a processor unit (CPU) with multiple cores, meaning that they are capable of working on multiple tasks at the same time. 32 | Modern operating systems with multi-tasking functionality (e.g the ability to run & manage multiple programs/windows) have a special component called a thread scheduler.

33 |

Each program you run can have multiple threads, and the scheduler's job is to distribute threads to the CPU's cores.

34 |

A web page's JavaScript normally executes on a single thread, meaning it will never use more than one CPU core. In most cases this is fine, and also helps ensures other tabs in the user's browser, or other programs, can also keep running smoothly.

35 |

However, some web applications might need to process a lot of data, or do a lot of expensive computing, and therefore can benefit from spreading work across all the available cores of the host machine.

36 |

Rinzler is a tool to do just that, in the simplest way possible - just define functions to run in parallel, and use native ES6 Promises to run & manage parallelized jobs.

37 |
38 | Infographic explaining how Rinzler allows you to use more CPU cores 39 |
40 | 41 |

Internally, it leverages Web Workers, which is a standard Web API for executing code in separate threads. It also includes a custom scheduler that handles spawning new Workers when necessary, sharing work between them, and shutting them down when they're not used.

42 | 43 | 44 |

Install

45 |
46 |
npm i rinzler-engine
 47 | 
48 |

Both ES & UMD modules are bundled, as well as TypeScript types, so you should be all set.

49 |

Rinzler targets browsers with WebWorkers and Promises support (check the browserslistrc). Most modern evergreen browsers, including Edge, should be compatible.

50 | 51 | 52 |

Quick start

53 |
54 |

In the following example, we will set up a Rinzler-accelerated app that decodes ArrayBuffers of text.

55 | 56 | 57 |

0. Environment initialization (optional)

58 |
59 |

In most real-life use cases, the job processing you will offload to Rinzler will depend on some dynamic variable in the context of your app: in this example, the original encoding of the text we want to decode.

60 |

The processing functions you will pass to Rinzler cannot contain references to external variables, because their source code will be extracted and printed in the Worker instances' source.

61 |

To work around this limitation, Rinzler allows you to setup an "initialization" function and pass it a payload. This function & payload will be run on each new Web Worker instance before it starts processing your jobs.

62 |
const initOptions = [{
encoding: 'utf-8' // We're just going to print this here, but in real life you would probably get this option from user input.
}]

function init(options) {
// This will run once in new Web Worker contexts. We can use the `self` global to store data for later.
self.params = {
encoding: options.encoding
}
} 63 |
64 | 65 | 66 |

1. Job processing function

67 |
68 |

We need to setup a function that will actually do the job we need to parallelize, in this case, decoding text buffers.

69 |
function processJob(message) {
// We expect to receive an object with an `encodedText` prop that is an ArrayBuffer.
const buffer = message.encodedText

// Get the encoding parameter we stored earlier, or default to ASCII.
const encoding = self.params?.encoding || 'ascii'

const text = new TextDecoder(encoding).decode(buffer)
return [text]
} 70 |
71 | 72 | 73 |

2. Engine start

74 |
75 |

Next we will import Rinzler and start the engine by passing the function(s) we defined above.

76 |

The following code is written for asynchronous contexts, but you can translate it to synchronous by using .then() with a callback instead of await.

77 |
import RinzlerEngine from 'rinzler-engine'

const engine = await new RinzlerEngine().configureAndStart(processJob, init, initOptions) 78 |
79 | 80 | 81 |

3. Running jobs

82 |
83 |

Now we can actually run jobs! We'll use the runJob() method, which returns a Promise that will resolve when the job is completed.

84 |

Since we need to pass an ArrayBuffer, we'll use the second argument as a Transferable[] - much like in the native worker.postMessage() API.

85 |
// Encode some text to try our decoder with
const encodedText = new TextEncoder('utf-8').encode('hello Rinzler!')

// Pass the encoded text to our decoding engine
const decodedResult = await engine.runJob({ encodedText }, [encodedText])

console.log(decodedResult) // "hello Rinzler!" 86 |
87 |

You can start as many jobs as you want, and take full advantage of ES6's asynchronous syntax (for example, Promise.all()).

88 |

If you use TypeScript, you can pass return types with the runJob<T>(): Promise<T> signature.

89 |

Under the hood, Rinzler will take care of launching Web Workers, balancing their load, and gracefully shutting them down when needed to reduce your app's memory footprint.

90 | 91 | 92 |

4. Shutting down

93 |
94 |

Web Worker instances will be destroyed by the browser when the page exits, but you can schedule a graceful shutdown yourself using engine.shutdown(), which returns a Promise that will resolve once all currently active jobs have completed and all workers have been stopped.

95 | 96 | 97 |

Licensing

98 |
99 |

Rinzler is licensed under the MIT License. You may integrate it in commercial applications.

100 |
101 | 102 | 103 |
© 2020-2022 Gabriel Saillard gabriel@saillard.dev
104 | 105 |
106 |
130 |
131 |

Generated using TypeDoc

132 |
-------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | Rinzler - v1.1.2
2 |
3 | 10 |
11 |
12 |
13 |
14 |

Rinzler - v1.1.2

15 |
16 |
17 |

Index

18 |
19 |

Classes

20 |
22 |
23 |

Type Aliases

24 |
28 |
52 |
53 |

Generated using TypeDoc

54 |
-------------------------------------------------------------------------------- /docs/types/WorkerFunction.html: -------------------------------------------------------------------------------- 1 | WorkerFunction | Rinzler - v1.1.2
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias WorkerFunction<T>

18 |
WorkerFunction<T>: ((message?: T) => Promise<WorkerFunctionTransferArgs> | WorkerFunctionTransferArgs)
19 |
20 |

Type Parameters

21 |
    22 |
  • 23 |

    T = unknown

    24 |

    Anything passed in runJob, with Transferable objects inlined.

    25 |
26 |
27 |

Type declaration

28 |
    29 |
  • 30 |
      31 |
    • (message?: T): Promise<WorkerFunctionTransferArgs> | WorkerFunctionTransferArgs
    • 32 |
    • 33 |

      The function which will process all jobs sent to this engine instance. May be sync or async. 34 | Worker functions cannot use variables defined outside of their block, because the function code itself is inlined 35 | and written to the Web Worker source code.

      36 |

      You can safely throw errors in this function as they will be catched and bubbled up from the engine.

      37 |
      38 |
      39 |

      Parameters

      40 |
        41 |
      • 42 |
        Optional message: T
      43 |

      Returns Promise<WorkerFunctionTransferArgs> | WorkerFunctionTransferArgs

46 |
70 |
71 |

Generated using TypeDoc

72 |
-------------------------------------------------------------------------------- /docs/types/WorkerFunctionTransferArgs.html: -------------------------------------------------------------------------------- 1 | WorkerFunctionTransferArgs | Rinzler - v1.1.2
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias WorkerFunctionTransferArgs

18 |
WorkerFunctionTransferArgs: [message?: unknown, transfer?: Transferable[]]
19 |

Interface for passing messages and Transferable data to Web Worker instances.

20 |

See DedicatedWorkerGlobalScope.postMessage() for more details.

21 |
24 |
48 |
49 |

Generated using TypeDoc

50 |
-------------------------------------------------------------------------------- /docs/types/WorkerInitFunction.html: -------------------------------------------------------------------------------- 1 | WorkerInitFunction | Rinzler - v1.1.2
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias WorkerInitFunction<T>

18 |
WorkerInitFunction<T>: ((message?: T) => Promise<void> | void)
19 |
20 |

Type Parameters

21 |
    22 |
  • 23 |

    T = unknown

    24 |

    Anything passed as initArgs in configureAndStart, with Transferable objects inlined.

    25 |
26 |
27 |

Type declaration

28 |
    29 |
  • 30 |
      31 |
    • (message?: T): Promise<void> | void
    • 32 |
    • 33 |

      A function for setting up a Web Worker environment before it starts processing jobs. May be sync or async. 34 | Worker functions cannot use variables defined outside of their block, because the function code itself is inlined 35 | and written to the Web Worker source code.

      36 |

      You should pass any dynamic data that won't change between jobs using initArgs in configureAndStart, 37 | and parse/use them in the init function.

      38 |

      If you need to store some global state to be used later when processing jobs, you can write a property to self, which will be a DedicatedWorkerGlobalScope. 39 | But be careful not to overwrite any browser-initialized properties in there!

      40 |
      41 |
      42 |

      Parameters

      43 |
        44 |
      • 45 |
        Optional message: T
      46 |

      Returns Promise<void> | void

49 |
73 |
74 |

Generated using TypeDoc

75 |
-------------------------------------------------------------------------------- /media/infographic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSquared/rinzler/82f040dba91d6c6b302dc24bab92167512ddd8f5/media/infographic.png -------------------------------------------------------------------------------- /media/rinzler_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSquared/rinzler/82f040dba91d6c6b302dc24bab92167512ddd8f5/media/rinzler_cover.png -------------------------------------------------------------------------------- /media/rinzler_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSquared/rinzler/82f040dba91d6c6b302dc24bab92167512ddd8f5/media/rinzler_logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rinzler-engine", 3 | "version": "1.1.2", 4 | "description": "A parallel processing engine for the browser.", 5 | "main": "./dist/rinzler.umd.js", 6 | "module": "./dist/rinzler.esm.js", 7 | "types": "./dist/types/index.d.ts", 8 | "scripts": { 9 | "prepare": "husky install", 10 | "lint": "eslint --color --ignore-path .gitignore \"src/**/*.ts\" \"worker-src/**/*.ts\"", 11 | "test": "tsc -p src && tsc -p worker-src", 12 | "build": "rollup --config", 13 | "make-docs": "typedoc && echo 'rinzler.js.org' > docs/CNAME" 14 | }, 15 | "files": [ 16 | "dist", 17 | "README.md", 18 | "LICENSE" 19 | ], 20 | "lint-staged": { 21 | "./*.js": "eslint --cache", 22 | "./src/**/*.ts": "eslint --cache", 23 | "./worker-src/**/*.ts": "eslint --cache" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/GitSquared/rinzler.git" 28 | }, 29 | "author": "Gabriel SAILLARD (https://gaby.dev)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/GitSquared/rinzler/issues" 33 | }, 34 | "homepage": "https://github.com/GitSquared/rinzler#readme", 35 | "dependencies": { 36 | "nanoid": "^3.3.4" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.20.2", 40 | "@babel/plugin-proposal-class-properties": "^7.18.6", 41 | "@babel/preset-env": "^7.20.2", 42 | "@babel/preset-typescript": "^7.18.6", 43 | "@commitlint/cli": "^17.6.1", 44 | "@commitlint/config-conventional": "^12.1.4", 45 | "@rollup/plugin-babel": "^5.3.1", 46 | "@rollup/plugin-node-resolve": "^13.3.0", 47 | "@rollup/plugin-replace": "^2.4.2", 48 | "@types/node": "^15.14.9", 49 | "@typescript-eslint/eslint-plugin": "^4.33.0", 50 | "@typescript-eslint/parser": "^4.33.0", 51 | "eslint": "^7.32.0", 52 | "husky": "^6.0.0", 53 | "komit": "^1.0.5", 54 | "lint-staged": "^13.2.1", 55 | "rollup": "^2.79.1", 56 | "rollup-plugin-analyzer": "^4.0.0", 57 | "rollup-plugin-copy": "^3.4.0", 58 | "rollup-plugin-license": "^2.9.1", 59 | "rollup-plugin-progress": "^1.1.2", 60 | "rollup-plugin-string": "^3.0.0", 61 | "rollup-plugin-terser": "^7.0.2", 62 | "rollup-plugin-typescript2": "^0.34.1", 63 | "tslib": "^2.4.1", 64 | "typedoc": "^0.23.20", 65 | "typedoc-plugin-rename-defaults": "^0.6.4", 66 | "typescript": "^4.8.4" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { string } from 'rollup-plugin-string' 3 | import { nodeResolve } from '@rollup/plugin-node-resolve' 4 | import typescript from 'rollup-plugin-typescript2' 5 | import babel from '@rollup/plugin-babel' 6 | import replace from '@rollup/plugin-replace' 7 | import copy from 'rollup-plugin-copy' 8 | import { terser } from 'rollup-plugin-terser' 9 | import license from 'rollup-plugin-license' 10 | import progress from 'rollup-plugin-progress' 11 | import analyze from 'rollup-plugin-analyzer' 12 | import pkg from './package.json' 13 | 14 | export default [ 15 | { 16 | input: 'worker-src/index.ts', 17 | plugins: [ 18 | copy({ 19 | targets: [ 20 | { src: 'worker-src/index.d.ts', dest: 'dist/internals', rename: () => { return 'worker-src.d.ts' } } 21 | ] 22 | }), 23 | typescript({ 24 | check: false, 25 | tsconfig: './worker-src/tsconfig.json' 26 | }), 27 | babel({ 28 | extensions: ['.js', '.ts'], 29 | babelHelpers: 'bundled', 30 | include: ['worker-src/**/*'] 31 | }), 32 | progress(), 33 | analyze({ 34 | summaryOnly: true 35 | }), 36 | terser() 37 | ], 38 | output: { 39 | file: 'dist/internals/worker-src.js', 40 | format: 'iife' 41 | } 42 | }, 43 | { 44 | input: 'src/index.ts', 45 | plugins: [ 46 | string({ 47 | include: './dist/internals/worker-src.js' 48 | }), 49 | nodeResolve({ 50 | browser: true 51 | }), 52 | typescript({ 53 | check: false, 54 | useTsconfigDeclarationDir: true, 55 | tsconfig: './src/tsconfig.json' 56 | }), 57 | replace({ 58 | preventAssignment: true, 59 | /* eslint-disable-next-line @typescript-eslint/naming-convention */ 60 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE) 61 | }), 62 | babel({ 63 | extensions: ['.js', '.ts'], 64 | babelHelpers: 'bundled', 65 | include: ['src/**/*'] 66 | }), 67 | license({ 68 | sourcemap: true, 69 | banner: { 70 | content: { 71 | file: path.join(__dirname, 'LICENSE') 72 | } 73 | } 74 | }), 75 | progress(), 76 | analyze({ 77 | summaryOnly: true 78 | }), 79 | terser() 80 | ], 81 | output: [ 82 | { 83 | file: pkg.main, 84 | name: 'RinzlerEngine', 85 | sourcemap: true, 86 | exports: 'default', 87 | format: 'umd' 88 | }, 89 | { 90 | file: pkg.module, 91 | sourcemap: true, 92 | format: 'esm' 93 | } 94 | ] 95 | }, 96 | ] 97 | -------------------------------------------------------------------------------- /src/event-emitter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 2 | type Listener = (data?: T) => unknown 3 | 4 | export default abstract class RinzlerEventEmitter { 5 | #listeners: Map = new Map() 6 | #onceListeners: Map = new Map() 7 | 8 | on(event: string, func: Listener): void { 9 | this.#listeners.get(event)?.push(func) || this.#listeners.set(event, [func]) 10 | } 11 | 12 | once(event: string, func: Listener): void { 13 | this.#onceListeners.get(event)?.push(func) || this.#onceListeners.set(event, [func]) 14 | } 15 | 16 | waitFor(event: string): Promise { 17 | return new Promise(resolve => { 18 | this.once(event, (e: unknown) => { 19 | resolve(e as T) 20 | }) 21 | }) 22 | } 23 | 24 | protected async _triggerEvent(event: string, data?: T): Promise { 25 | this.#listeners.get(event)?.forEach(listener => { 26 | listener(data) 27 | }) 28 | if (this.#onceListeners.has(event)) { 29 | const listeners = this.#onceListeners.get(event) 30 | this.#onceListeners.set(event, []) 31 | listeners?.map(listener => { 32 | listener(data) 33 | }) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerFunction, WorkerInitFunction, WorkerFunctionTransferArgs } from './worker-wrapper' 2 | import Scheduler from './scheduler' 3 | import WebWorker from './worker-wrapper' 4 | import calculateMedian from './median' 5 | 6 | export type { WorkerFunction, WorkerInitFunction, WorkerFunctionTransferArgs } 7 | 8 | /** 9 | ### Welcome to Rinzler's full documentation. 10 | If you're just getting started, check out the [Quick Start guide](../index.html#quick-start). 11 | 12 | This page describes the interface of an instanted RinzlerEngine class, which you can access like so: 13 | 14 | ```js 15 | import RinzlerEngine from 'rinzler-engine' 16 | 17 | const engine = new RinzlerEngine() 18 | ``` 19 | */ 20 | export default class RinzlerEngine { 21 | /* Internal props */ 22 | 23 | #maxTemp = (navigator.hardwareConcurrency && navigator.hardwareConcurrency > 1) ? navigator.hardwareConcurrency - 1 : 1 24 | #targetTemp = -1 25 | #minTemp = 1 26 | #workerArgs?: ConstructorParameters 27 | #scheduler: Scheduler = new Scheduler() 28 | #coolingTimer: [number, string] | null = null 29 | #extendPoolTimes: number[] = [] 30 | 31 | readonly #coolingDelay = 4000 32 | readonly #extendPoolTimesBacklogSize = 20 33 | 34 | /* Public methods */ 35 | 36 | /** 37 | Start the engine. 38 | 39 | Internally, configures the job processing functions, starts the load balancer and launches a first Web Worker. 40 | 41 | @param workFunction The function which will process all jobs sent to this engine instance. 42 | 43 | @param initFunction A function for setting up a Web Worker environment before it starts processing jobs. May be sync or async. 44 | 45 | @param initArgs Dynamic data to send to the initFunction to set up the environment of new Web Worker instances. 46 | 47 | @returns A Promise for the current engine instance. Can be used for chaining multiple instance method calls. 48 | 49 | @category Lifecycle 50 | */ 51 | async configureAndStart(workFunction: WorkerFunction, initFunction?: WorkerInitFunction, initArgs?: WorkerFunctionTransferArgs): Promise { 52 | if (this.#scheduler.workerPool.size) throw new Error('Rinzler: engine is already started, please use shutdown() first') 53 | 54 | this.#workerArgs = [workFunction, initFunction, initArgs] 55 | this.#targetTemp = 0 56 | await this.#heatUp() 57 | return this 58 | } 59 | 60 | /** 61 | Schedule, execute and get the results of a job. 62 | 63 | @typeParam T The return type of a successful job - meaning what the {@linkcode WorkerFunction} will return, with `Transferable` objects inlined. 64 | 65 | @returns A Promise that will be fulfilled with T or an empty resolve when the job has been processed. 66 | 67 | If the job threw an error when processing, the Promise will reject with an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Instance_properties) object containing the job's error message. 68 | 69 | @category Lifecycle 70 | */ 71 | async runJob(...args: WorkerFunctionTransferArgs): Promise { 72 | await this.#automaticHeatUp() 73 | return this.#scheduler.submitJob(args[0], args[1]) 74 | } 75 | 76 | /** 77 | Stop the engine. 78 | 79 | Wait for any remaining tasks to complete, shuts down all WebWorkers and sets the engine instance back to a blank state. 80 | 81 | @category Lifecycle 82 | */ 83 | async shutdown(): Promise { 84 | await this.#coolDown(this.#scheduler.workerPool.size + 1, true) 85 | } 86 | 87 | /** 88 | Start as much WebWorkers as currently recommended by the browser, or allowed by {@link afterburner}. 89 | 90 | You should use this only if you know you will have to process many jobs and want to ensure that the engine is fully revved up. 91 | In most cases the automatic "temperature" adjustement will provide a good balance between performance and memory footprint. 92 | 93 | You should immediatly follow this call with your actual {@link runJob} calls, otherwise the engine might start to cool itself down automatically. 94 | 95 | @returns A Promise for the current engine instance. Can be used for chaining multiple instance method calls. 96 | 97 | @category Optimization 98 | */ 99 | async preHeat(): Promise { 100 | await this.#heatUp(this.#maxTemp - this.#targetTemp) 101 | return this 102 | } 103 | 104 | /** 105 | ***WARNING: Possibly unstable.** 106 | 107 | Overrides the maximum number of WebWorkers Rinzler will attempt to spawn. 108 | 109 | Normally this is retrieved from [`navigator.hardwareConcurrency`](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorConcurrentHardware/hardwareConcurrency), which is a browser-provided indicator of how many Workers a browsing context should use, with respect to the browser's own limitations and the number of available CPU cores on the host hardware. 110 | 111 | By going beyond that number you may crash the browser tab, get unexpected errors, or a considerable boost in parallelized performance. 112 | Aim for the moon, right? 113 | 114 | @param max The new maximum number of WebWorkers this instance will manage. If set to lower than the current max, extra Workers will be gracefully shut down to respect the new limit. 115 | 116 | @returns A Promise for the current engine instance. Can be used for chaining multiple instance method calls. 117 | 118 | @category ⚠️ Dangerous 119 | */ 120 | async afterburner(max: number): Promise { 121 | const current = this.#measureTemp() 122 | this.#maxTemp = max 123 | if ((current - max) > 0) { 124 | await this.#coolDown(current - max) 125 | } 126 | 127 | return this 128 | } 129 | 130 | /* Debug method */ 131 | 132 | _debug(): Record { 133 | return { 134 | maxTemp: this.#maxTemp, 135 | targetTemp: this.#targetTemp, 136 | temp: this.#measureTemp(), 137 | minTemp: this.#minTemp, 138 | scheduler: this.#scheduler, 139 | coolingTimer: this.#coolingTimer, 140 | coolingDelay: this.#coolingDelay, 141 | medianExtendPoolTime: this.#measureMedianForkTime() 142 | } 143 | } 144 | 145 | /* Internal methods */ 146 | 147 | #measureTemp(): number { 148 | return this.#scheduler.workerPool.size 149 | } 150 | 151 | #measureMedianForkTime(): number { 152 | return calculateMedian(this.#extendPoolTimes) 153 | } 154 | 155 | async #automaticHeatUp(): Promise { 156 | const { jobCount } = this.#scheduler.getLeastBusyWorker() 157 | if (jobCount * this.#scheduler.measureMedianExecTime() > this.#measureMedianForkTime()) { 158 | // Forking a new thread is worth it, let's rock 159 | try { 160 | await this.#heatUp() 161 | } catch { 162 | // We're already maxed out! 163 | } 164 | } 165 | } 166 | 167 | async #heatUp(threadCount = 1): Promise { 168 | // start more workers 169 | if (!this.#workerArgs) throw new Error('Rinzler (internal): unknown parameters for heating up new threads') 170 | if ((this.#targetTemp + threadCount) > this.#maxTemp) throw new Error('Rinzler (internal): attempting to overheat') 171 | if (this.#targetTemp < 0) throw new Error('Rinzler (internal): engine is too cold, likely has already been shut down') 172 | 173 | this.#targetTemp = (this.#measureTemp() + threadCount) || threadCount 174 | for (let i = 0; i < threadCount; i++) { 175 | const perfMarkA = performance.now() 176 | 177 | const worker = new WebWorker(...this.#workerArgs) 178 | const wid = await this.#scheduler.extendPool(worker) 179 | worker.on('idle', () => { 180 | this.#automaticCoolDown(wid) 181 | }) 182 | await worker.start() 183 | 184 | const perfMarkB = performance.now() 185 | this.#extendPoolTimes.unshift(perfMarkB - perfMarkA) 186 | this.#extendPoolTimes.splice(this.#extendPoolTimesBacklogSize, this.#extendPoolTimes.length - this.#extendPoolTimesBacklogSize) 187 | } 188 | } 189 | 190 | #automaticCoolDown(wid?: string): void { 191 | if (!wid) { 192 | const { wid, jobCount } = this.#scheduler.getLeastBusyWorker() 193 | if (jobCount === 0) { 194 | this.#automaticCoolDown(wid) 195 | } 196 | return 197 | } 198 | 199 | if (this.#coolingTimer) return 200 | if (this.#measureTemp() <= this.#minTemp) { 201 | this.#coolingTimer = null 202 | return 203 | } 204 | const worker = this.#scheduler.workerPool.get(wid) 205 | if (!worker) return 206 | 207 | this.#coolingTimer = [window.setTimeout(async () => { 208 | try { 209 | await this.#coolDown(1, false, wid) 210 | } catch { 211 | // Worker does not exist anymore or engine is already cooled 212 | } 213 | this.#coolingTimer = null 214 | this.#automaticCoolDown() 215 | }, this.#coolingDelay), wid] 216 | 217 | worker.once('jobok', () => { 218 | if (this.#coolingTimer && this.#coolingTimer[1] === wid) { 219 | window.clearTimeout(this.#coolingTimer[0]) 220 | this.#coolingTimer = null 221 | // Check if there is another idle worker to launch a new timer on 222 | this.#automaticCoolDown() 223 | } 224 | }) 225 | } 226 | 227 | async #coolDown(threadCount = 1, override = false, wid?: string): Promise { 228 | // reduce number of active workers 229 | if (!override) { 230 | if ((this.#targetTemp - threadCount) < this.#minTemp) throw new Error('Rinzler (internal): attempting to underheat') 231 | if ((this.#targetTemp - threadCount) <= 0) throw new Error('Rinzler (internal): engine cannot run in subzero temperatures') 232 | } 233 | 234 | if (wid && threadCount !== 1) throw new Error('Rinzler (internal): cannot cool down more than 1 workers when specifying a worked id') 235 | 236 | this.#targetTemp = this.#measureTemp() - threadCount 237 | for (let i = 0; i < threadCount; i++) { 238 | (wid) ? await this.#scheduler.reducePool(wid) : await this.#scheduler.reducePool() 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/median.ts: -------------------------------------------------------------------------------- 1 | export default function calculateMedian(arr: number[]): number { 2 | if (!arr.length) return 0 3 | const mid = Math.ceil(arr.length /2) 4 | const nums = [...arr].sort((a, b) => a - b) 5 | return (arr.length % 2 !== 0) ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2 6 | } 7 | -------------------------------------------------------------------------------- /src/scheduler.ts: -------------------------------------------------------------------------------- 1 | import type WebWorker from './worker-wrapper' 2 | import type { JobReturnCall, JobCall } from './worker-wrapper' 3 | import calculateMedian from './median' 4 | import { nanoid } from 'nanoid' 5 | 6 | export default class Scheduler { 7 | /* Public props */ 8 | 9 | workerPool: Map = new Map() 10 | pressure = 0 11 | lastJobsExecTimes: number[] = [] 12 | 13 | /* Internal props */ 14 | 15 | readonly #jobsExecTimesBacklogSize = 20 16 | 17 | /* Public methods */ 18 | 19 | async extendPool(worker: WebWorker): Promise { 20 | const id = nanoid() 21 | this.workerPool.set(id, worker) 22 | return id 23 | } 24 | 25 | async reducePool(wid?: string): Promise { 26 | let worker, id 27 | if (wid) { 28 | [worker, id] = [ 29 | this.workerPool.get(wid), 30 | wid 31 | ] 32 | } else { 33 | ({ worker, wid: id } = this.getLeastBusyWorker()) 34 | } 35 | if (!worker) return 36 | this.workerPool.delete(id) 37 | await worker.shutdown() 38 | } 39 | 40 | async submitJob(message: unknown, transfer?: Transferable[]): Promise { 41 | const id = nanoid() 42 | const job: JobCall = { 43 | type: 'job', 44 | id, 45 | message, 46 | transfer 47 | } 48 | 49 | const { worker } = this.getLeastBusyWorker() 50 | worker.submitJob(job) 51 | this.pressure++ 52 | 53 | await worker.waitFor(`jobok-${id}`) 54 | const perfMarkA = performance.now() 55 | const jobResults = await worker.waitFor>(`jobdone-${id}`) 56 | this.pressure-- 57 | 58 | const perfMarkB = performance.now() 59 | this.lastJobsExecTimes.unshift(perfMarkB - perfMarkA) 60 | this.lastJobsExecTimes.splice(this.#jobsExecTimesBacklogSize, this.lastJobsExecTimes.length - this.#jobsExecTimesBacklogSize) 61 | 62 | if (jobResults.error) { 63 | throw new Error('(in Rinzler job): ' + jobResults.message) 64 | } 65 | 66 | return jobResults.message 67 | } 68 | 69 | measureMedianExecTime(): number { 70 | return calculateMedian(this.lastJobsExecTimes) 71 | } 72 | 73 | getLeastBusyWorker(): { worker: WebWorker; wid: string; jobCount: number } { 74 | let lowest: [number, string] = [-1, ''] 75 | // O(n) complexity except when a worker has 0 current jobs, 76 | // but we're unlikely to ever be looping >20 entries, so... 77 | for (const entry of this.workerPool) { 78 | if (lowest[0] === -1 || entry[1].jobs.length < lowest[0]) { 79 | if (!entry[1].active) continue 80 | lowest = [entry[1].jobs.length, entry[0]] 81 | if (lowest[0] === 0) break 82 | } 83 | } 84 | return { 85 | worker: this.workerPool.get(lowest[1]) as WebWorker, 86 | wid: lowest[1], 87 | jobCount: lowest[0] 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["index.ts"], 4 | "compilerOptions": { 5 | "lib": ["esnext", "dom"], 6 | "isolatedModules": true, 7 | "resolveJsonModule": true, 8 | "declaration": true, 9 | "declarationDir": "../dist/types", 10 | "sourceMap": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/worker-wrapper.ts: -------------------------------------------------------------------------------- 1 | import RinzlerEventEmitter from './event-emitter' 2 | import src from '../dist/internals/worker-src.js' 3 | 4 | interface InternalWorkerMsg { 5 | type: string 6 | } 7 | 8 | export interface InitCall extends InternalWorkerMsg { 9 | type: 'init' 10 | message?: unknown 11 | transfer?: Transferable[] 12 | } 13 | 14 | export interface JobCall extends InternalWorkerMsg { 15 | type: 'job' 16 | id: string 17 | message?: unknown 18 | transfer?: Transferable[] 19 | } 20 | 21 | export interface JobAcceptCall extends InternalWorkerMsg { 22 | type: 'jobok' 23 | id: string 24 | } 25 | 26 | export interface JobReturnCall extends InternalWorkerMsg { 27 | type: 'jobdone' 28 | id: string 29 | error: boolean 30 | message: T 31 | } 32 | 33 | /** 34 | Interface for passing messages and `Transferable` data to Web Worker instances. 35 | 36 | See [`DedicatedWorkerGlobalScope.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) for more details. 37 | */ 38 | export type WorkerFunctionTransferArgs = [message?: unknown, transfer?: Transferable[]] 39 | 40 | /** 41 | A function for setting up a Web Worker environment before it starts processing jobs. May be sync or async. 42 | Worker functions cannot use variables defined outside of their block, because the function code itself is inlined 43 | and written to the Web Worker source code. 44 | 45 | You should pass any dynamic data that won't change between jobs using `initArgs` in {@link RinzlerEngine.configureAndStart}, 46 | and parse/use them in the init function. 47 | 48 | If you need to store some global state to be used later when processing jobs, you can write a property to `self`, which will be a [`DedicatedWorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope). 49 | But be careful not to overwrite any browser-initialized properties in there! 50 | 51 | @typeParam T Anything passed as `initArgs` in {@link RinzlerEngine.configureAndStart}, with `Transferable` objects inlined. 52 | */ 53 | export type WorkerInitFunction = (message?: T) => Promise | void 54 | 55 | /** 56 | The function which will process all jobs sent to this engine instance. May be sync or async. 57 | Worker functions cannot use variables defined outside of their block, because the function code itself is inlined 58 | and written to the Web Worker source code. 59 | 60 | You can safely throw errors in this function as they will be catched and bubbled up from the engine. 61 | 62 | @typeParam T Anything passed in {@link RinzlerEngine.runJob}, with `Transferable` objects inlined. 63 | */ 64 | export type WorkerFunction = (message?: T) => Promise | WorkerFunctionTransferArgs 65 | 66 | export default class WebWorker extends RinzlerEventEmitter { 67 | /* Public props */ 68 | 69 | jobs: JobCall[] = [] 70 | active = false 71 | 72 | /* Internal props */ 73 | 74 | #workerRef: Worker 75 | #workingJob: string | null = null 76 | #initArgs?: WorkerFunctionTransferArgs 77 | 78 | /* Public methods */ 79 | 80 | constructor(workFunction: WorkerFunction, initFunction?: WorkerInitFunction, initArgs?: WorkerFunctionTransferArgs) { 81 | super() 82 | const populatedSrc = src 83 | .replace('INIT_FUNCTION', initFunction?.toString() || 'function(){}') 84 | .replace('WORK_FUNCTION', workFunction.toString()) 85 | 86 | const srcBlob = new Blob([populatedSrc], { type: 'application/javascript' }) 87 | 88 | this.#workerRef = new Worker(URL.createObjectURL(srcBlob)) 89 | this.#workerRef.addEventListener('message', this.#messageHandler.bind(this)) 90 | this.#workerRef.addEventListener('messageerror', this.#errMessageHandler.bind(this)) 91 | this.#workerRef.addEventListener('error', this.#errorHandler.bind(this)) 92 | 93 | this.#initArgs = initArgs 94 | } 95 | 96 | async start(): Promise { 97 | const initMsg: InitCall = { 98 | type: 'init', 99 | message: this.#initArgs && this.#initArgs[0], 100 | transfer: this.#initArgs && this.#initArgs[1] 101 | } 102 | this.#workerRef.postMessage(initMsg) 103 | await super.waitFor('ready') 104 | this.active = true 105 | super._triggerEvent('idle') 106 | } 107 | 108 | submitJob(job: JobCall): void { 109 | if (!this.active) throw new Error('Rinzler WorkerWrapper: not taking new jobs') 110 | 111 | this.jobs.push(job) 112 | this.#processQueue() 113 | } 114 | 115 | async shutdown(): Promise { 116 | this.active = false 117 | if (this.jobs.length > 0) { 118 | await this.waitFor('idle') 119 | } 120 | this.terminate() 121 | } 122 | 123 | terminate(): void { 124 | return this.#workerRef.terminate() 125 | } 126 | 127 | /* Internal methods */ 128 | 129 | async #processQueue(): Promise { 130 | if (this.#workingJob !== null) return 131 | const job = this.jobs[0] 132 | this.#workingJob = job.id 133 | this.#workerRef.postMessage(job, job.transfer || []) 134 | await super.waitFor(`jobok-${job.id}`) 135 | } 136 | 137 | #messageHandler(e: MessageEvent): void { 138 | switch(e.data.type) { 139 | case 'ready': 140 | super._triggerEvent('ready') 141 | break 142 | case 'jobok': 143 | super._triggerEvent('jobok', e.data) 144 | super._triggerEvent(`jobok-${e.data.id}`, e.data) 145 | break 146 | case 'jobdone': 147 | super._triggerEvent('jobdone', e.data) 148 | super._triggerEvent(`jobdone-${e.data.id}`, e.data) 149 | this.jobs.splice(this.jobs.map(j => j.id).indexOf(e.data.id), 1) 150 | this.#workingJob = null 151 | if (this.jobs.length === 0) { 152 | super._triggerEvent('idle') 153 | } else { 154 | this.#processQueue() 155 | } 156 | break 157 | default: 158 | throw new Error('Rinzler WorkerWrapper: unknown message received') 159 | } 160 | } 161 | 162 | #errMessageHandler(): void { 163 | throw new Error('Rinzler WorkerWrapper: failed to deserialize message') 164 | } 165 | 166 | #errorHandler(e: ErrorEvent): void { 167 | throw new Error('Rinzler WorkerWrapper internal error: ' + e.message) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "allowSyntheticDefaultImports": true, 6 | "moduleResolution": "node", 7 | "importsNotUsedAsValues": "error", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "noEmit": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "useDefineForClassFields": true, 13 | "baseUrl": "./" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": "src/index.ts", 3 | "tsconfig": "src/tsconfig.json", 4 | "out": "docs", 5 | "name": "Rinzler", 6 | "theme": "default", 7 | "lightHighlightTheme": "github-light", 8 | "darkHighlightTheme": "github-dark", 9 | "includeVersion": true, 10 | "externalPattern": "worker-src/**/*.ts", 11 | "excludeExternals": true, 12 | "excludePrivate": true, 13 | "excludeProtected": true 14 | } 15 | -------------------------------------------------------------------------------- /worker-src/index.d.ts: -------------------------------------------------------------------------------- 1 | export default '' 2 | -------------------------------------------------------------------------------- /worker-src/index.ts: -------------------------------------------------------------------------------- 1 | import type { InitCall, JobCall, JobAcceptCall, JobReturnCall, WorkerInitFunction, WorkerFunction, WorkerFunctionTransferArgs } from '../src/worker-wrapper' 2 | 3 | interface ReceivedMessageEvent extends MessageEvent { 4 | data: InitCall | JobCall 5 | } 6 | 7 | declare const self: DedicatedWorkerGlobalScope 8 | 9 | /* These two will be replaced by actual code when launching the worker - see src/worker-wrapper */ 10 | 11 | declare const INIT_FUNCTION: WorkerInitFunction 12 | declare const WORK_FUNCTION: WorkerFunction 13 | 14 | /* Where the actual magic happens! */ 15 | 16 | async function processJob(job: JobCall): Promise { 17 | const id = job.id 18 | 19 | self.postMessage({ 20 | type: 'jobok', 21 | id 22 | } as JobAcceptCall) 23 | 24 | let [message, transfer]: WorkerFunctionTransferArgs = [] 25 | let error = false 26 | try { 27 | [message, transfer] = await WORK_FUNCTION(job.message) 28 | } catch(err: unknown) { 29 | message = (err as Error).message ?? err 30 | error = true 31 | } 32 | 33 | self.postMessage({ 34 | type: 'jobdone', 35 | id, 36 | error, 37 | message 38 | } as JobReturnCall, transfer || []) 39 | } 40 | 41 | /* Attach event handlers */ 42 | 43 | self.addEventListener('message', async (e: ReceivedMessageEvent) => { 44 | switch(e.data.type) { 45 | case 'init': 46 | await INIT_FUNCTION(e.data.message) 47 | self.postMessage({ type: 'ready' }) 48 | break 49 | case 'job': 50 | processJob(e.data) 51 | break 52 | default: 53 | throw new Error('Rinzler Worker: unknown message received') 54 | } 55 | }) 56 | self.addEventListener('messageerror', () => { 57 | throw new Error('Rinzler Worker: failed to deserialize message') 58 | }) 59 | -------------------------------------------------------------------------------- /worker-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["esnext", "webworker"], 5 | "declaration": false 6 | } 7 | } 8 | --------------------------------------------------------------------------------