├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── __tests__ ├── base.all.test.ts ├── chunkSchedulers │ ├── animationFrame.browser.test.ts │ ├── custom.all.test.ts │ ├── getChunkScheduler.browser.test.ts │ ├── getChunkScheduler.nodejs.test.ts │ ├── idleCallback.browser.test.ts │ ├── immediate.nodejs.test.ts │ ├── postMessage.browser.test.ts │ ├── timeout.browser.test.ts │ └── timeout.nodejs.test.ts └── utils.ts ├── jasmine.json ├── karma.conf.ts ├── package-lock.json ├── package.json ├── rollup.config.ts ├── src ├── chunkSchedulers │ ├── animationFrame.ts │ ├── idleCallback.ts │ ├── immediate.ts │ ├── index.ts │ ├── postMessage.ts │ ├── timeout.ts │ └── types.ts ├── index.ts ├── scheduler.ts ├── types.ts └── utils.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json", 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["@typescript-eslint", "sonarjs"], 8 | "rules": { 9 | "@typescript-eslint/array-type": "error", 10 | "@typescript-eslint/comma-dangle": "error", 11 | "@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }], 12 | "@typescript-eslint/naming-convention": [ 13 | "error", 14 | { 15 | "selector": "typeParameter", 16 | "format": ["PascalCase"], 17 | "prefix": ["T"] 18 | } 19 | ], 20 | "@typescript-eslint/member-delimiter-style": [ 21 | "error", 22 | { 23 | "multiline": { "delimiter": "semi", "requireLast": true }, 24 | "singleline": { "delimiter": "semi", "requireLast": true } 25 | } 26 | ], 27 | "@typescript-eslint/no-explicit-any": "error", 28 | "@typescript-eslint/no-extra-parens": ["error", "all", {"nestedBinaryExpressions": false }], 29 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 30 | "@typescript-eslint/type-annotation-spacing": "error", 31 | "array-bracket-spacing": "error", 32 | "arrow-spacing": "error", 33 | "block-spacing": "error", 34 | "camelcase": "error", 35 | "curly": "error", 36 | "func-call-spacing": "error", 37 | "generator-star-spacing": ["error", { "before": false, "after": true, "anonymous": "neither" }], 38 | "indent": ["error", 4, { "SwitchCase": 1 }], 39 | "key-spacing": "error", 40 | "max-len": ["error", { "code": 120 }], 41 | "max-statements-per-line": ["error", { "max": 1 }], 42 | "no-console": "error", 43 | "no-dupe-keys": "error", 44 | "no-duplicate-imports": "error", 45 | "no-else-return": "error", 46 | "no-extra-semi": "error", 47 | "no-irregular-whitespace": "error", 48 | "no-multi-spaces": "error", 49 | "no-multiple-empty-lines": ["error", { "max": 1 }], 50 | "no-negated-condition": "error", 51 | "no-self-assign": "error", 52 | "no-trailing-spaces": "error", 53 | "no-unexpected-multiline": "error", 54 | "no-unreachable": "error", 55 | "no-unused-expressions": "error", 56 | "no-useless-return": "error", 57 | "no-var": "error", 58 | "no-with": "error", 59 | "object-curly-spacing": ["error", "always"], 60 | "object-shorthand": "error", 61 | "one-var": ["error", "never"], 62 | "one-var-declaration-per-line": "error", 63 | "operator-assignment": "error", 64 | "prefer-arrow-callback": "error", 65 | "prefer-const": "error", 66 | "prefer-object-spread": "error", 67 | "require-yield": "error", 68 | "rest-spread-spacing": "error", 69 | "quotes": ["error", "single"], 70 | "semi": "error", 71 | "semi-spacing": "error", 72 | "sonarjs/no-all-duplicated-branches": "error", 73 | "sonarjs/no-collapsible-if": "error", 74 | "sonarjs/no-duplicated-branches": "error", 75 | "sonarjs/no-extra-arguments": "error", 76 | "sonarjs/no-identical-conditions": "error", 77 | "sonarjs/no-identical-expressions": "error", 78 | "sonarjs/no-inverted-boolean-check": "error", 79 | "sonarjs/no-one-iteration-loop": "error", 80 | "sonarjs/no-redundant-boolean": "error", 81 | "sonarjs/no-redundant-jump": "error", 82 | "sonarjs/no-same-line-conditional": "error", 83 | "sonarjs/no-use-of-empty-return-value": "error", 84 | "sonarjs/prefer-immediate-return": "error", 85 | "sonarjs/prefer-object-literal": "error", 86 | "sonarjs/prefer-single-boolean-return": "error", 87 | "space-before-function-paren": ["error", "never"], 88 | "space-in-parens": "error", 89 | "space-infix-ops": "error", 90 | "space-unary-ops": "error", 91 | "switch-colon-spacing": "error" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: 15 | - 14 16 | - 12 17 | name: Node.js ${{ matrix.node-version }} 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | - name: Install Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2-beta 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: npm i 27 | - name: Lint 28 | run: npm run lint 29 | - name: Test 30 | run: npm run test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rpt2_cache 2 | .vscode 3 | lib 4 | node_modules 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | commit-hooks = false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Dmitry Filatov 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 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LRT 2 | ![Tests](https://github.com/dfilatov/lrt/workflows/Tests/badge.svg?branch=master) 3 | [![NPM Version](https://img.shields.io/npm/v/lrt.svg?style=flat-square)](https://www.npmjs.com/package/lrt) 4 | 5 | ## What is it? 6 | LRT is a scheduler for long-running tasks inside browsers and Node.JS. 7 | 8 | ## Key features 9 | * API to split long-running tasks into units of work via Iterator protocol 10 | * Ability to run multiple long-running tasks concurrently with coordinating their execution via coopeative scheduling 11 | * Ability to abort outdated tasks 12 | * Ability to specify chunk budget and maximize its utilization 13 | * Built-in set of predefined chunk schedulers 14 | * Ability to implement custom chunk scheduler 15 | * Supports generators for tasks splitting 16 | * Works in both Browser and Node.JS platforms 17 | * Small, fast and dependency-free 18 | 19 | The main idea is to split long-running tasks into small units of work joined into chunks with limited budget of execution time. Units of works are executed synchronously until budget of current chunk is reached, afterwards thread is unblocked until scheduler executes next chunk and so on until all tasks have been completed. 20 | 21 | lrt 22 | 23 | ## Table of Contents 24 | * [Installation](#installation) 25 | * [Usage](#usage) 26 | * [API](#api) 27 | * [Scheduler](#scheduler) 28 | * [Task iterator](#task-iterator) 29 | * [Chunk scheduler](#chunk-scheduler) 30 | * [Questions and answers](#questions-and-answers) 31 | * [Example](#example) 32 | 33 | ## Installation 34 | ``` 35 | $ npm install lrt 36 | ``` 37 | **Note**: LRT requires native [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) and [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) so if your environment doesn't support them, you will have to install any suitable polyfills as well. 38 | 39 | ## Usage 40 | ```ts 41 | // with ES6 modules 42 | import { createScheduler } from 'lrt'; 43 | 44 | // with CommonJS modules 45 | const { createScheduler } = require('lrt'); 46 | ``` 47 | 48 | ## API 49 | 50 | ```ts 51 | const scheduler = createScheduler(options); 52 | ``` 53 | * `options` (`object`, optional) 54 | * `options.chunkBudget` (`number`, optional, default is `10`) An execution budget of chunk in milliseconds. 55 | * `options.chunkScheduler` (`string|object`, optional, default is `'auto'`) A [chunk scheduler](#chunk-scheduler), can be `'auto'`, `'idleCallback'`, `'animationFrame'`, `'immediate'`, `'timeout'` or object representing custom scheduler. 56 | 57 | Returned `scheduler` has two methods: 58 | * `const task = scheduler.runTask(taskIterator)` 59 | Runs task with a given [taskIterator](#task-iterator) and returns task (promise) resolved or rejected after task has completed or thrown an error respectively. 60 | * `scheduler.abortTask(task)` Aborts task execution as soon as possible (see diagram above). 61 | 62 | ### Scheduler 63 | Scheduler is responsible for tasks running, aborting and coordinating order of execution of their units. It accumulates statistics while tasks are being run and tries to maximize budget utilization of each chunk. If a unit of some task has no time to be executed in the current chunk, it will get higher priority to be executed in the next chunk. 64 | 65 | ### Task iterator 66 | Task iterator should be an object implementing [Iterator protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol). The most convenient way to build iterator is to use generators (calling a generator function returns a generator object implementing iterator protocol). Another option is to build your own object implementing iterator protocol. 67 | 68 | Example with generator: 69 | ```ts 70 | function* generator() { 71 | let i = 0; 72 | 73 | while(i < 10) { 74 | doCurrentPartOfTask(i); 75 | i++; 76 | yield; 77 | } 78 | 79 | return i; 80 | } 81 | 82 | const iterator = generator(); 83 | ``` 84 | 85 | Example with object implementing iterator protocol: 86 | ```ts 87 | const iterator = { 88 | next(i = 0) { 89 | doCurrentPartOfTask(i); 90 | 91 | return { 92 | done: i < 10, 93 | value: i + 1 94 | }; 95 | } 96 | }; 97 | ``` 98 | For convenience LRT passes a previous value as an argument to the `next` method. The first `next` call doesn't obtain this argument and default value can be specified as an initial one. 99 | 100 | ### Chunk scheduler 101 | Chunk scheduler is utilized internally to schedule execution of the next chunk of units. Built-in options: 102 | * `'auto'` (by default) LRT will try to detect the best available option for your current environment. 103 | In browsers any of `'idleCallback'` / `'animationFrame'` / `'postMessage'` option will be used depending on their availability, or `'immediate'` inside NodeJS. If nothing suitable is available, `'timeout'` option will be used as a fallback. 104 | * `'idleCallback'` LRT will try to use [Background Tasks API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API). If it's not available, `'timeout'` option will be used as a fallback. 105 | * `'animationFrame'` LRT will try to use [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame). If your tasks need to change the DOM, you should use it instead `'auto'` or `'idleCallback'`. If it's not available, `'timeout'` option will be used as a fallback. 106 | * `'postMessage'` LRT will try to use [postMessage](https://developer.mozilla.org/ru/docs/Web/API/Window/postMessage). If it's not available, `'timeout'` option will be used as a fallback. 107 | * `'immediate'` LRT will try to use [setImmediate](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate). If it's not available, `'timeout'` option will be used as a fallback. 108 | * `'timeout'` LRT will use [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) with zero delay. 109 | 110 | Also you can specify your own implementation of scheduler. 111 | 112 | #### Custom chunk scheduler 113 | Custom scheduler should implement two methods: 114 | * `request(fn)` (required) Accepts function `fn` and returns `token` for possible aborting via `cancel` method (if it is specified) 115 | * `cancel(token)` (optional) Accepts `token` and cancels scheduling 116 | 117 | For example, let's implement scheduler which runs next chunk of units in ~100 milliseconds after previous chunk has ended: 118 | ```ts 119 | const customChunkScheduler = { 120 | request: fn => setTimeout(fn, 100), 121 | cancel: token => clearTimeout(token) 122 | }; 123 | 124 | const scheduler = createScheduler({ 125 | chunkScheduler: customChunkScheduler 126 | }); 127 | ``` 128 | 129 | ## Questions and answers 130 | 131 | **What if unit takes more time than chunk budget?** 132 | 133 | More likely this means that chunk budget is too small or you need to split your tasks into smaller units. Anyway LRT guarantees at least one of units of some task will be executed within each chunk. 134 | 135 | **Why not just move long-running task into Web Worker?** 136 | 137 | Despite the fact that Web Workers are very useful, they do have a cost: time to instantiate/terminate workers, message latency on large workloads, need for coordination between threads, lack of access the DOM. Nevertheless, you can use LRT inside Web Worker and get the best of both worlds: do not affect main thread and have ability to abort outdated tasks. 138 | 139 | ## Example 140 | ```ts 141 | // Create scheduler 142 | const scheduler = createScheduler(); 143 | 144 | // Imitate a part of some long-running task taking 80ms in the whole 145 | function doPartOfTask1() { 146 | const startTime = Date.now(); 147 | 148 | while(Date.now() - startTime < 8) {} 149 | } 150 | 151 | // Imitate a part of another long-running task taking 100ms in the whole 152 | function doPartOfTask2() { 153 | const startTime = Date.now(); 154 | 155 | while(Date.now() - startTime < 5) {} 156 | } 157 | 158 | function* task1Generator() { 159 | let i = 0; 160 | 161 | while(i < 10) { // 10 units will be executed 162 | doPartOfTask1(); 163 | i++; 164 | yield; 165 | } 166 | 167 | return i; 168 | } 169 | 170 | function* task2Generator() { 171 | let i = 0; 172 | 173 | while(i < 20) { // 20 units will be executed 174 | doPartOfTask2(); 175 | i++; 176 | yield; 177 | } 178 | 179 | return i; 180 | } 181 | 182 | // Run both tasks concurrenly 183 | const task1 = scheduler.runTask(task1Generator()); 184 | const task2 = scheduler.runTask(task2Generator()); 185 | 186 | // Wait until first task has been completed 187 | task1.then( 188 | result => { 189 | console.log(result); // prints "10" 190 | }, 191 | err => { 192 | console.error(err); 193 | } 194 | ); 195 | 196 | // Abort second task in 50 ms, it won't be completed 197 | setTimeout(() => scheduler.abortTask(task2), 50); 198 | ``` 199 | -------------------------------------------------------------------------------- /__tests__/base.all.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler } from '../src'; 2 | import { emptyGenerator, sleep } from './utils'; 3 | 4 | describe('runTask', () => { 5 | it('should fulfill promise with result after task has completed', done => { 6 | createScheduler().runTask((function*() { 7 | let i = 0; 8 | 9 | while(i++ < 9) { 10 | sleep(10); 11 | yield; 12 | } 13 | 14 | return i; 15 | })()).then(result => { 16 | expect(result).toBe(10); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should complete task even its iteration takes more time than budget', done => { 22 | createScheduler({ chunkBudget: 5 }).runTask((function*() { 23 | let i = 0; 24 | 25 | while(i++ < 9) { 26 | sleep(10); 27 | yield; 28 | } 29 | 30 | return i; 31 | })()).then(result => { 32 | expect(result).toBe(10); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should run a couple of tasks concurrently', done => { 38 | const scheduler = createScheduler(); 39 | const order: string[] = []; 40 | 41 | Promise.all([ 42 | scheduler.runTask((function*() { 43 | let i = 0; 44 | 45 | while(i < 5) { 46 | sleep(10); 47 | order.push('unit1'); 48 | i++; 49 | yield; 50 | } 51 | 52 | return i; 53 | })()), 54 | scheduler.runTask((function*() { 55 | let i = 0; 56 | 57 | while(i < 9) { 58 | sleep(10); 59 | order.push('unit2'); 60 | i += 2; 61 | yield; 62 | } 63 | 64 | return i; 65 | })()) 66 | ]).then(([result1, result2]) => { 67 | expect(order) 68 | .toEqual(['unit1', 'unit2', 'unit1', 'unit2', 'unit1', 'unit2', 'unit1', 'unit2', 'unit1', 'unit2']); 69 | expect(result1).toBe(5); 70 | expect(result2).toBe(10); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('should reject promise if unit throws exception', done => { 76 | const err = new Error(); 77 | 78 | createScheduler().runTask((function*() { 79 | let i = 0; 80 | 81 | while(i < 9) { 82 | sleep(10); 83 | 84 | if(i++ > 4) { 85 | throw err; 86 | } 87 | 88 | yield; 89 | } 90 | 91 | return i; 92 | })()).catch(_err => { 93 | expect(_err).toBe(err); 94 | done(); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('abortTask', () => { 100 | it('should be aborted after a while', done => { 101 | const scheduler = createScheduler(); 102 | const task = scheduler.runTask((function*() { 103 | let i = 0; 104 | 105 | while(i++ < 9) { 106 | sleep(10); 107 | yield; 108 | } 109 | 110 | return i; 111 | })()); 112 | 113 | task.finally( 114 | () => { 115 | done.fail('Aborted task mustn\'t be completed'); 116 | } 117 | ); 118 | 119 | setTimeout(() => scheduler.abortTask(task), 50); 120 | setTimeout(() => done(), 300); 121 | }); 122 | 123 | it('should be aborted immediately', done => { 124 | const scheduler = createScheduler(); 125 | const task = scheduler.runTask(emptyGenerator()); 126 | 127 | task.finally( 128 | () => { 129 | done.fail('Aborted task mustn\'t be completed'); 130 | } 131 | ); 132 | 133 | scheduler.abortTask(task); 134 | 135 | setTimeout(() => done(), 100); 136 | }); 137 | 138 | it('should not affect other pending tasks', done => { 139 | const scheduler = createScheduler(); 140 | const task1 = scheduler.runTask((function*() { 141 | let i = 0; 142 | 143 | while(i++ < 5) { 144 | sleep(10); 145 | yield; 146 | } 147 | 148 | return i; 149 | })()); 150 | const task2 = scheduler.runTask((function*() { 151 | let i = 0; 152 | 153 | while(i++ < 9) { 154 | sleep(10); 155 | yield; 156 | } 157 | 158 | return i; 159 | })()); 160 | 161 | scheduler.abortTask(task1); 162 | 163 | task1.then( 164 | () => { 165 | done.fail('Aborted task mustn\'t be completed'); 166 | }, 167 | () => { 168 | done.fail('Aborted task mustn\'t be rejected'); 169 | } 170 | ); 171 | 172 | task2.then(result => { 173 | expect(result).toBe(10); 174 | done(); 175 | }); 176 | }); 177 | }); 178 | 179 | describe('chunking', () => { 180 | it('should request chunk after budget has been reached', done => { 181 | const order: string[] = []; 182 | const scheduler = createScheduler({ 183 | chunkScheduler: { 184 | request(fn): void { 185 | order.push('chunk'); 186 | fn(); 187 | } 188 | }, 189 | chunkBudget: 50 190 | }); 191 | 192 | scheduler.runTask((function*() { 193 | let i = 0; 194 | 195 | while(i < 5) { 196 | sleep(20); 197 | order.push('unit'); 198 | i++; 199 | yield; 200 | } 201 | 202 | return i; 203 | })()).then(() => { 204 | expect(order).toEqual(['chunk', 'unit', 'unit', 'chunk', 'unit', 'unit', 'chunk', 'unit']); 205 | done(); 206 | }); 207 | }); 208 | 209 | it('should not request chunk if task aborted immediately', () => { 210 | const requestMock = jasmine.createSpy(); 211 | const scheduler = createScheduler({ 212 | chunkScheduler: { 213 | request: requestMock 214 | } 215 | }); 216 | 217 | const task = scheduler.runTask(emptyGenerator()); 218 | 219 | scheduler.abortTask(task); 220 | 221 | expect(requestMock).not.toHaveBeenCalled(); 222 | }); 223 | 224 | it('should cancel chunk if last task is aborted', done => { 225 | const cancelMock = jasmine.createSpy(); 226 | const scheduler = createScheduler({ 227 | chunkScheduler: { 228 | request: fn => setTimeout(fn, 50), 229 | cancel: (token: number) => { 230 | clearTimeout(token); 231 | cancelMock(); 232 | } 233 | } 234 | }); 235 | 236 | const task1 = scheduler.runTask(emptyGenerator()); 237 | const task2 = scheduler.runTask(emptyGenerator()); 238 | 239 | setTimeout( 240 | () => { 241 | scheduler.abortTask(task1); 242 | expect(cancelMock).not.toHaveBeenCalled(); 243 | scheduler.abortTask(task2); 244 | expect(cancelMock.calls.count()).toEqual(1); 245 | done(); 246 | }, 247 | 20 248 | ); 249 | }); 250 | }); 251 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/animationFrame.browser.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler, Scheduler } from '../../src'; 2 | import { simpleGenerator } from '../utils'; 3 | 4 | describe('animationFrame chunk scheduler', () => { 5 | let scheduler: Scheduler; 6 | 7 | beforeEach(() => { 8 | scheduler = createScheduler({ 9 | chunkScheduler: 'animationFrame' 10 | }); 11 | }); 12 | 13 | it('should use window.requestAnimationFrame()', done => { 14 | spyOn(window, 'requestAnimationFrame').and.callThrough(); 15 | 16 | scheduler.runTask(simpleGenerator()).then(() => { 17 | expect((window.requestAnimationFrame as jasmine.Spy).calls.count()).toEqual(10); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/custom.all.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler, ChunkScheduler, Scheduler } from '../../src'; 2 | import { simpleGenerator } from '../utils'; 3 | 4 | describe('custom chunk scheduler', () => { 5 | const customChunkScheduler: ChunkScheduler = { 6 | request: fn => globalThis.setTimeout(fn, 50), 7 | cancel: token => globalThis.clearTimeout(token) 8 | }; 9 | let scheduler: Scheduler; 10 | 11 | beforeEach(() => { 12 | scheduler = createScheduler({ 13 | chunkScheduler: customChunkScheduler 14 | }); 15 | }); 16 | 17 | it('should use custom implementation', done => { 18 | spyOn(customChunkScheduler, 'request').and.callThrough(); 19 | 20 | scheduler.runTask(simpleGenerator()).then(() => { 21 | expect((customChunkScheduler.request as jasmine.Spy).calls.count()).toEqual(10); 22 | done(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/getChunkScheduler.browser.test.ts: -------------------------------------------------------------------------------- 1 | import { getChunkScheduler } from '../../src/chunkSchedulers'; 2 | import { timeoutChunkScheduler } from '../../src/chunkSchedulers/timeout'; 3 | import { animationFrameChunkScheduler } from '../../src/chunkSchedulers/animationFrame'; 4 | import { idleCallbackChunkScheduler } from '../../src/chunkSchedulers/idleCallback'; 5 | 6 | describe('getChunkScheduler()', () => { 7 | it('should properly detect supported chunk scheduler', () => { 8 | expect(getChunkScheduler('animationFrame')).toBe(animationFrameChunkScheduler!); 9 | expect(getChunkScheduler('immediate')).toBe(timeoutChunkScheduler); 10 | expect(getChunkScheduler(['immediate', 'animationFrame'])).toBe(animationFrameChunkScheduler!); 11 | expect(getChunkScheduler(['immediate'])).toBe(timeoutChunkScheduler); 12 | expect(getChunkScheduler(['auto'])).toBe(idleCallbackChunkScheduler!); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/getChunkScheduler.nodejs.test.ts: -------------------------------------------------------------------------------- 1 | import { getChunkScheduler } from '../../src/chunkSchedulers'; 2 | import { timeoutChunkScheduler } from '../../src/chunkSchedulers/timeout'; 3 | import { immediateChunkScheduler } from '../../src/chunkSchedulers/immediate'; 4 | 5 | describe('getChunkScheduler()', () => { 6 | it('should properly detect supported chunk scheduler', () => { 7 | expect(getChunkScheduler('animationFrame')).toBe(timeoutChunkScheduler); 8 | expect(getChunkScheduler(['animationFrame', 'idleCallback'])).toBe(timeoutChunkScheduler); 9 | expect(getChunkScheduler('immediate')).toBe(immediateChunkScheduler!); 10 | expect(getChunkScheduler(['animationFrame', 'immediate'])).toBe(immediateChunkScheduler!); 11 | expect(getChunkScheduler(['animationFrame'])).toBe(timeoutChunkScheduler); 12 | expect(getChunkScheduler(['auto'])).toBe(immediateChunkScheduler!); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/idleCallback.browser.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler, Scheduler } from '../../src'; 2 | import { simpleGenerator } from '../utils'; 3 | 4 | describe('idleCallback chunk scheduler', () => { 5 | let scheduler: Scheduler; 6 | 7 | beforeEach(() => { 8 | scheduler = createScheduler({ 9 | chunkScheduler: 'idleCallback' 10 | }); 11 | }); 12 | 13 | it('should use window.requestIdleCallback()', done => { 14 | spyOn(window, 'requestIdleCallback').and.callThrough(); 15 | 16 | scheduler.runTask(simpleGenerator()).then(() => { 17 | expect((window.requestIdleCallback as jasmine.Spy).calls.count()).toEqual(10); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/immediate.nodejs.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler, Scheduler } from '../../src'; 2 | import { simpleGenerator } from '../utils'; 3 | 4 | describe('immediate chunk scheduler', () => { 5 | let scheduler: Scheduler; 6 | 7 | beforeEach(() => { 8 | scheduler = createScheduler({ 9 | chunkScheduler: 'immediate' 10 | }); 11 | }); 12 | 13 | it('should use setImmediate()', done => { 14 | spyOn(globalThis, 'setImmediate').and.callThrough(); 15 | 16 | scheduler.runTask(simpleGenerator()).then(() => { 17 | expect((globalThis.setImmediate as unknown as jasmine.Spy).calls.count()).toEqual(10); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/postMessage.browser.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler, Scheduler } from '../../src'; 2 | import { simpleGenerator } from '../utils'; 3 | 4 | describe('postMessage chunk scheduler', () => { 5 | let scheduler: Scheduler; 6 | 7 | beforeEach(() => { 8 | scheduler = createScheduler({ 9 | chunkScheduler: 'postMessage' 10 | }); 11 | }); 12 | 13 | it('should use window.postMessage()', done => { 14 | spyOn(window, 'postMessage').and.callThrough(); 15 | 16 | scheduler.runTask(simpleGenerator()).then(() => { 17 | expect((window.postMessage as jasmine.Spy).calls.count()).toEqual(10); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/timeout.browser.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler, Scheduler } from '../../src'; 2 | import { simpleGenerator } from '../utils'; 3 | 4 | describe('timeout chunk scheduler', () => { 5 | let scheduler: Scheduler; 6 | 7 | beforeEach(() => { 8 | scheduler = createScheduler({ 9 | chunkScheduler: 'timeout' 10 | }); 11 | }); 12 | 13 | it('should use window.setTimeout()', done => { 14 | spyOn(window, 'setTimeout').and.callThrough(); 15 | 16 | scheduler.runTask(simpleGenerator()).then(() => { 17 | expect((window.setTimeout as unknown as jasmine.Spy).calls.count()).toEqual(10); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/chunkSchedulers/timeout.nodejs.test.ts: -------------------------------------------------------------------------------- 1 | import { createScheduler, Scheduler } from '../../src'; 2 | import { simpleGenerator } from '../utils'; 3 | 4 | describe('timeout chunk scheduler', () => { 5 | let scheduler: Scheduler; 6 | 7 | beforeEach(() => { 8 | scheduler = createScheduler({ 9 | chunkScheduler: 'timeout' 10 | }); 11 | }); 12 | 13 | it('should use setTimeout()', done => { 14 | spyOn(globalThis, 'setTimeout').and.callThrough(); 15 | 16 | scheduler.runTask(simpleGenerator()).then(() => { 17 | expect((globalThis.setTimeout as unknown as jasmine.Spy).calls.count()).toEqual(10); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import { now } from '../src/utils'; 2 | 3 | function* emptyGenerator(): Iterator { 4 | yield; 5 | } 6 | 7 | function sleep(ms: number): void { 8 | const startTime = now(); 9 | 10 | while(now() - startTime < ms) {} 11 | } 12 | 13 | const simpleGenerator = function*(): Iterator { 14 | let i = 0; 15 | 16 | while(i++ < 9) { 17 | sleep(10); 18 | yield i; 19 | } 20 | 21 | return i; 22 | }; 23 | 24 | export { 25 | emptyGenerator, 26 | simpleGenerator, 27 | sleep 28 | }; 29 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "__tests__", 3 | "spec_files": [ 4 | "**/*.all.test.ts", 5 | "**/*.nodejs.test.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 2 | 3 | module.exports = (config: { set(options: Record): void; }) => { 4 | config.set({ 5 | basePath: '.', 6 | frameworks: ['jasmine', 'karma-typescript'], 7 | browsers: ['ChromeHeadless'], 8 | preprocessors: { 9 | '**/*.ts': 'karma-typescript' 10 | }, 11 | karmaTypescriptConfig: { 12 | include: [ 13 | '__tests__/**/*.all.test.ts', 14 | '__tests__/**/*.browser.test.ts' 15 | ], 16 | bundlerOptions: { 17 | exclude: ['perf_hooks'] 18 | } 19 | }, 20 | files: [ 21 | '__tests__/**/*.ts', 22 | 'src/**/*.ts' 23 | ], 24 | exclude: [ 25 | '__tests__/**/*.nodejs.test.ts' 26 | ] 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lrt", 3 | "version": "3.1.1", 4 | "description": "Module to split long-running tasks into chunks with limited budget", 5 | "keywords": [ 6 | "cooperative", 7 | "scheduler", 8 | "chunks", 9 | "budget", 10 | "task", 11 | "long-running", 12 | "abortable", 13 | "javascript", 14 | "typescript" 15 | ], 16 | "devDependencies": { 17 | "@rollup/plugin-replace": "4.0.0", 18 | "@typescript-eslint/parser": "5.36.2", 19 | "@typescript-eslint/eslint-plugin": "5.36.2", 20 | "@types/jasmine": "4.3.0", 21 | "@types/node": "16.11.6", 22 | "eslint": "8.23.0", 23 | "eslint-plugin-sonarjs": "0.15.0", 24 | "jasmine": "3.9.0", 25 | "jasmine-ts": "0.4.0", 26 | "karma": "6.4.0", 27 | "karma-chrome-launcher": "3.1.1", 28 | "karma-jasmine": "5.1.0", 29 | "karma-typescript": "5.5.3", 30 | "puppeteer": "17.1.1", 31 | "rollup": "2.79.0", 32 | "rollup-plugin-typescript2": "0.33.0", 33 | "simple-git-hooks": "2.8.0", 34 | "ts-node": "10.9.1", 35 | "typescript": "4.8.2" 36 | }, 37 | "files": [ 38 | "lib" 39 | ], 40 | "main": "lib/index.js", 41 | "browser": "lib/index.browser.js", 42 | "types": "lib/types/index.d.ts", 43 | "scripts": { 44 | "build": "rm -rf lib && npx rollup -c rollup.config.ts && BROWSER=true npx rollup -c rollup.config.ts", 45 | "lint": "npx tsc --noEmit && npx eslint --ext ts src __tests__", 46 | "postpublish": "git push --follow-tags --no-verify", 47 | "prepublishOnly": "npm run build", 48 | "test": "npx jasmine-ts --config=jasmine.json && npx karma start --single-run" 49 | }, 50 | "author": "Dmitry Filatov ", 51 | "repository": { 52 | "type": "git", 53 | "url": "git://github.com/dfilatov/lrt.git" 54 | }, 55 | "license": "MIT", 56 | "simple-git-hooks": { 57 | "pre-push": "npm run lint && npm test" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import replace from '@rollup/plugin-replace'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import * as pkg from './package.json'; 4 | 5 | const BROWSER = !!process.env.BROWSER; 6 | const rollupConfig = { 7 | input: './src/index.ts', 8 | output: { 9 | name: pkg.name, 10 | file: BROWSER ? pkg.browser : pkg.main, 11 | format: 'cjs' 12 | }, 13 | plugins: [ 14 | typescript({ 15 | useTsconfigDeclarationDir: true, 16 | tsconfigOverride: { 17 | include: ['src/**/*'] 18 | } 19 | }), 20 | replace({ 21 | 'process.env.BROWSER': JSON.stringify(BROWSER), 22 | preventAssignment: true 23 | }) 24 | ] 25 | }; 26 | 27 | export default rollupConfig; 28 | -------------------------------------------------------------------------------- /src/chunkSchedulers/animationFrame.ts: -------------------------------------------------------------------------------- 1 | import { ChunkScheduler } from './types'; 2 | 3 | const animationFrameChunkScheduler: ChunkScheduler | null = 4 | typeof window !== 'undefined' && 5 | typeof window.requestAnimationFrame === 'function' && 6 | typeof window.cancelAnimationFrame === 'function' ? 7 | { 8 | request: fn => window.requestAnimationFrame(fn), 9 | cancel: token => window.cancelAnimationFrame(token) 10 | } : 11 | null; 12 | 13 | export { 14 | animationFrameChunkScheduler 15 | }; 16 | -------------------------------------------------------------------------------- /src/chunkSchedulers/idleCallback.ts: -------------------------------------------------------------------------------- 1 | import { ChunkScheduler } from './types'; 2 | 3 | const idleCallbackChunkScheduler: ChunkScheduler | null = 4 | typeof window !== 'undefined' && 5 | typeof window.requestIdleCallback === 'function' && 6 | typeof window.cancelIdleCallback === 'function' ? 7 | { 8 | request: fn => window.requestIdleCallback(fn), 9 | cancel: token => window.cancelIdleCallback(token) 10 | } : 11 | null; 12 | 13 | export { 14 | idleCallbackChunkScheduler 15 | }; 16 | -------------------------------------------------------------------------------- /src/chunkSchedulers/immediate.ts: -------------------------------------------------------------------------------- 1 | import { ChunkScheduler } from './types'; 2 | 3 | const immediateChunkScheduler: ChunkScheduler | null = 4 | typeof setImmediate === 'function' && 5 | typeof clearImmediate === 'function' ? 6 | { 7 | request: fn => setImmediate(fn), 8 | cancel: token => clearImmediate(token) 9 | } : 10 | null; 11 | 12 | export { 13 | immediateChunkScheduler 14 | }; 15 | -------------------------------------------------------------------------------- /src/chunkSchedulers/index.ts: -------------------------------------------------------------------------------- 1 | import { ChunkScheduler, ChunkSchedulerType } from './types'; 2 | import { animationFrameChunkScheduler } from './animationFrame'; 3 | import { idleCallbackChunkScheduler } from './idleCallback'; 4 | import { immediateChunkScheduler } from './immediate'; 5 | import { postMessageScheduler } from './postMessage'; 6 | import { timeoutChunkScheduler } from './timeout'; 7 | 8 | const BUILTIN_CHUNK_SHEDULERS: Record, ChunkScheduler | null> = { 9 | auto: 10 | idleCallbackChunkScheduler || 11 | animationFrameChunkScheduler || 12 | immediateChunkScheduler || 13 | postMessageScheduler, 14 | animationFrame: animationFrameChunkScheduler, 15 | idleCallback: idleCallbackChunkScheduler, 16 | immediate: immediateChunkScheduler, 17 | postMessage: postMessageScheduler, 18 | timeout: timeoutChunkScheduler 19 | }; 20 | 21 | function getChunkScheduler(type: ChunkSchedulerType | ChunkSchedulerType[]): ChunkScheduler { 22 | if(typeof type === 'string') { 23 | return BUILTIN_CHUNK_SHEDULERS[type] || timeoutChunkScheduler; 24 | } 25 | 26 | if(Array.isArray(type)) { 27 | for(let i = 0; i < type.length; i++) { 28 | const item = type[i]; 29 | 30 | if(typeof item === 'string') { 31 | const chunkScheduler = BUILTIN_CHUNK_SHEDULERS[item]; 32 | 33 | if(chunkScheduler) { 34 | return chunkScheduler; 35 | } 36 | } else { 37 | return item; 38 | } 39 | } 40 | 41 | return timeoutChunkScheduler; 42 | } 43 | 44 | return type; 45 | } 46 | 47 | export { 48 | ChunkSchedulerType, 49 | ChunkScheduler, 50 | 51 | getChunkScheduler 52 | }; 53 | -------------------------------------------------------------------------------- /src/chunkSchedulers/postMessage.ts: -------------------------------------------------------------------------------- 1 | import { ChunkScheduler } from './types'; 2 | 3 | const postMessageScheduler: ChunkScheduler | null = 4 | typeof window === 'undefined' ? 5 | null : 6 | (() => { 7 | const msg = '__lrt__' + Math.random(); 8 | let fns: (() => void)[] = []; 9 | 10 | window.addEventListener( 11 | 'message', 12 | e => { 13 | if(e.data === msg) { 14 | const fnsToCall = fns; 15 | 16 | fns = []; 17 | 18 | for(let i = 0; i < fnsToCall.length; i++) { 19 | fnsToCall[i](); 20 | } 21 | } 22 | }, 23 | true 24 | ); 25 | 26 | return { 27 | request: (fn: () => void) => { 28 | fns.push(fn); 29 | 30 | if(fns.length === 1) { 31 | window.postMessage(msg, '*'); 32 | } 33 | } 34 | }; 35 | })(); 36 | 37 | export { 38 | postMessageScheduler 39 | }; 40 | -------------------------------------------------------------------------------- /src/chunkSchedulers/timeout.ts: -------------------------------------------------------------------------------- 1 | import { ChunkScheduler } from './types'; 2 | 3 | const timeoutChunkScheduler: ChunkScheduler = { 4 | request: fn => setTimeout(fn, 0), 5 | cancel: token => clearTimeout(token) 6 | }; 7 | 8 | export { 9 | timeoutChunkScheduler 10 | }; 11 | -------------------------------------------------------------------------------- /src/chunkSchedulers/types.ts: -------------------------------------------------------------------------------- 1 | type ChunkSchedulerType = 2 | 'animationFrame' | 3 | 'idleCallback' | 4 | 'immediate' | 5 | 'postMessage' | 6 | 'timeout' | 7 | 'auto' | 8 | ChunkScheduler; 9 | 10 | interface ChunkScheduler { 11 | request(fn: () => void): T; 12 | cancel?(t: T): void; 13 | } 14 | 15 | export { 16 | ChunkSchedulerType, 17 | ChunkScheduler 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { createScheduler } from './scheduler'; 2 | export { Scheduler, SchedulerOptions } from './types'; 3 | export { ChunkScheduler, ChunkSchedulerType } from './chunkSchedulers'; 4 | -------------------------------------------------------------------------------- /src/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { getChunkScheduler } from './chunkSchedulers'; 2 | import { now, microtask } from './utils'; 3 | import { Scheduler, SchedulerOptions, Task } from './types'; 4 | 5 | const DEFAULT_CHUNK_SCHEDULER_TYPE = 'auto'; 6 | const DEFAULT_CHUNK_BUDGET = 10; 7 | 8 | function createScheduler({ 9 | chunkScheduler: chunkSchedulerType = DEFAULT_CHUNK_SCHEDULER_TYPE, 10 | chunkBudget = DEFAULT_CHUNK_BUDGET 11 | }: SchedulerOptions = {}): Scheduler { 12 | const pendingTasks = new Map, Task>(); 13 | const chunkScheduler = getChunkScheduler(chunkSchedulerType); 14 | let chunkSchedulerToken: unknown = null; 15 | let tasksOrder: Promise[] = []; 16 | 17 | function chunk(): void { 18 | chunkSchedulerToken = null; 19 | 20 | let iterationStartTime = now(); 21 | let checkBudget = false; 22 | let restChunkBudget = chunkBudget; 23 | const nextTasksOrder: Promise[] = []; 24 | 25 | while(tasksOrder.length > 0) { 26 | const taskPromise = tasksOrder.shift()!; 27 | const task = pendingTasks.get(taskPromise)!; 28 | let iterated = false; 29 | 30 | if(checkBudget && restChunkBudget < task.meanIterationElapsedTime) { 31 | nextTasksOrder.push(taskPromise); 32 | } 33 | else { 34 | checkBudget = true; 35 | 36 | try { 37 | const { value, done } = task.iterator.next(task.value); 38 | 39 | iterated = true; 40 | 41 | task.value = value; 42 | 43 | if(done) { 44 | pendingTasks.delete(taskPromise); 45 | task.resolve(value); 46 | } 47 | else { 48 | tasksOrder.push(taskPromise); 49 | } 50 | } 51 | catch(err) { 52 | pendingTasks.delete(taskPromise); 53 | task.reject(err); 54 | } 55 | } 56 | 57 | const iterationElapsedTime = now() - iterationStartTime; 58 | 59 | if(iterated) { 60 | task.iterationCount++; 61 | task.totalElapsedTime += iterationElapsedTime; 62 | task.meanIterationElapsedTime = task.totalElapsedTime / task.iterationCount; 63 | } 64 | 65 | restChunkBudget -= iterationElapsedTime; 66 | iterationStartTime += iterationElapsedTime; 67 | } 68 | 69 | if(nextTasksOrder.length > 0) { 70 | tasksOrder = nextTasksOrder; 71 | chunkSchedulerToken = chunkScheduler.request(chunk); 72 | } 73 | } 74 | 75 | return { 76 | runTask(taskIterator: Iterator): Promise { 77 | let task: Task; 78 | const taskPromise = new Promise((resolve, reject) => { 79 | task = { 80 | value: undefined, 81 | iterator: taskIterator, 82 | iterationCount: 0, 83 | meanIterationElapsedTime: 0, 84 | totalElapsedTime: 0, 85 | resolve, 86 | reject 87 | }; 88 | }); 89 | 90 | pendingTasks.set(taskPromise, task!); 91 | 92 | microtask(() => { 93 | // check if it's not already aborted 94 | if(!pendingTasks.has(taskPromise)) { 95 | return; 96 | } 97 | 98 | tasksOrder.push(taskPromise); 99 | 100 | if(tasksOrder.length === 1) { 101 | chunkSchedulerToken = chunkScheduler.request(chunk); 102 | } 103 | }); 104 | 105 | return taskPromise; 106 | }, 107 | 108 | abortTask(taskPromise: Promise): void { 109 | if(pendingTasks.delete(taskPromise)) { 110 | const taskOrderIdx = tasksOrder.indexOf(taskPromise); 111 | 112 | // task can be absent if it's added to pending tasks via `runTask` but then 113 | // `abortTask` is called synchronously before invoking microtask callback 114 | if(taskOrderIdx > -1) { 115 | tasksOrder.splice(taskOrderIdx, 1); 116 | 117 | if(tasksOrder.length === 0 && chunkScheduler.cancel && chunkSchedulerToken !== null) { 118 | chunkScheduler.cancel(chunkSchedulerToken); 119 | chunkSchedulerToken = null; 120 | } 121 | } 122 | } 123 | } 124 | }; 125 | } 126 | 127 | export { 128 | createScheduler 129 | }; 130 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ChunkSchedulerType } from './chunkSchedulers'; 2 | 3 | interface Scheduler { 4 | runTask(iterator: Iterator): Promise; 5 | abortTask(promise: Promise): void; 6 | } 7 | 8 | interface SchedulerOptions { 9 | chunkScheduler?: ChunkSchedulerType | ChunkSchedulerType[]; 10 | chunkBudget?: number; 11 | } 12 | 13 | interface Task { 14 | value: unknown; 15 | iterator: Iterator; 16 | iterationCount: number; 17 | meanIterationElapsedTime: number; 18 | totalElapsedTime: number; 19 | resolve(result: T): void; 20 | reject(reason: unknown): void; 21 | } 22 | 23 | export { 24 | Scheduler, 25 | SchedulerOptions, 26 | Task 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const now: () => number = typeof performance === 'object' && typeof performance.now === 'function' ? 2 | () => performance.now() : 3 | (() => { 4 | if(!process.env.BROWSER) { 5 | try { 6 | return require('perf_hooks').performance.now; 7 | } 8 | catch {} 9 | } 10 | 11 | return Date.now; 12 | })(); 13 | 14 | const microtaskPromise = Promise.resolve(); 15 | 16 | function microtask(fn: () => void): void { 17 | microtaskPromise.then(fn); 18 | } 19 | 20 | export { 21 | now, 22 | microtask 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "./lib", 5 | "target" : "es5", 6 | "declaration": true, 7 | "declarationDir": "./lib/types", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "sourceMap": true, 11 | "lib": [ 12 | "es2017", 13 | "dom" 14 | ] 15 | } 16 | } 17 | --------------------------------------------------------------------------------