├── .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 | 
3 | [](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 |
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 |
--------------------------------------------------------------------------------