├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docma.json ├── jest-puppeteer.config.js ├── jest-puppeteer.json ├── jest.json ├── lib ├── ITaskOptions.d.ts ├── ITaskTimerEvent.d.ts ├── ITaskTimerOptions.d.ts ├── ITimeInfo.d.ts ├── Task.d.ts ├── TaskCallback.d.ts ├── TaskTimer.d.ts ├── index.d.ts ├── tasktimer.js ├── tasktimer.min.js ├── tasktimer.min.js.map └── utils.d.ts ├── package-lock.json ├── package.json ├── src ├── ITaskOptions.ts ├── ITaskTimerEvent.ts ├── ITaskTimerOptions.ts ├── ITimeInfo.ts ├── Task.ts ├── TaskCallback.ts ├── TaskTimer.ts ├── index.ts └── utils.ts ├── tasktimer-logo.png ├── test ├── _server │ ├── index.js │ └── public │ │ ├── index.html │ │ └── manual-test.html ├── browser │ └── tasktimer.spec.js └── node │ ├── tasktimer.spec.ts │ └── utils.spec.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.xml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /backup/ 2 | /design/ 3 | /wiki/ 4 | /docs/ 5 | /node_modules/ 6 | /.grunt/ 7 | _SpecRunner.html 8 | src*.zip 9 | TODO.md 10 | .vscode 11 | /test/coverage/ 12 | perf-compare.spec.ts -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | - '6' 6 | before_script: cd $TRAVIS_BUILD_DIR 7 | script: 8 | - npm run test 9 | - npm run coveralls 10 | notifications: 11 | email: 12 | # recipients: 13 | # - one@example.com 14 | on_success: never # default: change 15 | on_failure: always # default: always 16 | 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `TaskTimer` Changelog 2 | 3 | All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). 4 | 5 | ## 3.0.0 (2019-08-02) 6 | 7 | ### Changed 8 | - **Breaking**: TypeScript type definitions now require TypeScript 3. 9 | - Updated dependencies to their latest versions. 10 | 11 | ### Fixed 12 | - An issue where `timer#time.elapsed` was a timestamp when idle but a timespan when running. Fixes [#11](https://github.com/onury/tasktimer/issues/11). 13 | 14 | 15 | ## 2.0.1 (2019-01-21) 16 | This release includes various **breaking changes**. Please see the [API reference][docs]. Also note that this version is completely re-written in TypeScript. 17 | 18 | ### Changed 19 | - **Breaking**: `TaskTimer` is no longer a default export. See _Usage_ section in readme. 20 | - **Breaking**: `TaskTimer#addTask()` renamed to `TaskTimer#add()`. This no longer accepts a `string` argument. It should either be an options object, a `Task` instance or a callback function. It also accepts an array of these, to add multiple tasks at once. 21 | - **Breaking**: `Task#name` renamed to `Task#id`. 22 | - **Breaking**: The task ID is optional (auto-generated when omitted) when task is created via `#add()`. But `callback` is now required. 23 | - **Breaking**: `TaskTimer#removeTask()` renamed to `TaskTimer#remove()`. 24 | - **Breaking**: `TaskTimer#getTask()` renamed to `TaskTimer#get()`. 25 | - **Breaking**: `TaskTimer.State` enumeration type is changed to `string`. (meaning enum values are also changed.) 26 | 27 | ### Added 28 | - Timer option: `precision: boolean` indicating whether the timer should auto-adjust the delay between ticks if it's off due to task loads or clock drifts. See more info in readme. Default: `true` 29 | - Timer option: `stopOnCompleted: boolean` indicating whether to automatically stop the timer when all tasks are completed. For this to take affect, all added tasks should have `totalRuns` and/or `stopDate` configured. Default: `false` 30 | - Support for async tasks. Use `callback(task: Task, done: Function)` signature. Either return a promise or call `done()` argument within the callback; when the task is done. 31 | - Task option: `enabled: boolean` indicating whether the task is currently enabled. This essentially gives you a manual control over execution. The task will always bypass the callback while this is set to `false`. 32 | - Task option: `tickDelay: number` to specify a number of ticks to allow before running the task for the first time. 33 | - Task option: `removeOnCompleted: number` indicating whether to remove the task (to free up memory) when task has completed its executions (runs). For this to take affect, the task should have `totalRuns` and/or `stopDate` configured. Default: `false` 34 | - Event: `TaskTimer.Event.TASK_COMPLETED` (`"taskCompleted"`) Emitted when a task has completed all of its executions (runs) or reached its stopping date/time (if set). Note that this event will only be fired if the tasks has a `totalRuns` limit or a `stopDate` value set. 35 | - Event: `TaskTimer.Event.COMPLETED` (`"completed"`) Emitted when *all* tasks have completed all of their executions (runs) or reached their stopping date/time (if set). Note that this event will only be fired if *each* task either have a `totalRuns` limit or a `stopDate` value set, or both. 36 | - Event: `TaskTimer.Event.TASK_ERROR` (`"taskError"`) Catches and emits errors produced (if any) on a task execution. 37 | - `Task#time` getter that returns an object `{ started, stopped, elapsed }` defining the life-time of a task. 38 | - `TaskTimer#runCount: boolean` indicating the total number of timer runs, including resumed runs. 39 | - `TaskTimer#taskRunCount: boolean` indicating the total number of all task executions (runs). 40 | - TypeScript support. 41 | 42 | ### Fixed 43 | - An issue where default task options would not be set in some cases. Fixes issue [#5](https://github.com/onury/tasktimer/issues/5). 44 | - An issue where webpack would mock or polyfill Node globals unnecessarily. (v2.0.1 patch) 45 | 46 | ### Removed 47 | - **Breaking**: `TaskTimer#resetTask()` is removed. Use `#get(name).reset()` to reset a task. 48 | - Dropped bower. Please use npm to install. 49 | - (Dev) Removed grunt in favour of npm scripts. Using jest instead of jasmine-core for tests. 50 | 51 | 52 | ## 1.0.0 (2016-08-16) 53 | 54 | - Initial release. 55 | 56 | 57 | [docs]:https://onury.io/tasktimer/api -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019, Onur Yıldırım . All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | TaskTimer 3 |

4 |

5 | Build Status 6 | Coverage Status 7 | Known Vulnerabilities 8 | Downloads 9 | License 10 |
11 | npm 12 | Release 13 | TypeScript 14 | Documentation 15 |
16 | © 2021, Onur Yıldırım (@onury). 17 |

18 |
19 | 20 | An accurate timer utility for running periodic tasks on the given interval ticks or dates. 21 | 22 | ## Why `TaskTimer`? 23 | Because of the single-threaded, asynchronous [nature of JavaScript][how-timers-work], each execution takes a piece of CPU time, and the time they have to wait will vary, depending on the load. This creates a latency and cumulative difference in asynchronous timers; that gradually increase the inacuraccy. `TaskTimer` claims to be the best timer that overcomes this problem as much as possible. 24 | 25 | Secondly, I needed a task manager that can handle multiple tasks on different intervals, with a single timer instance. 26 | 27 | ## Features 28 | - **Precission & Accuracy**: With the `precision` option (enabled by default); 29 | - The delay between each tick is **auto-adjusted** when it's off due to task/CPU loads or [clock drifts][clock-drift]. 30 | - In Node.js, `TaskTimer` also makes use of `process.hrtime()` **high-resolution real-time**. The time is relative to an arbitrary time in the past (not related to the time of day) and therefore not subject to clock drifts. 31 | - The timer may hit a synchronous / blocking task; or detect significant time drift (longer than the base interval) due to JS event queue, which cannot be recovered by simply adjusting the next delay. In this case, right from the next tick onward; it will auto-recover as much as possible by running "immediate" tasks until it reaches the proper time vs tick/run balance. 32 | - Run or schedule **multiple tasks** (on a single timer instance). 33 | - Ability to run **sync** or **async** tasks that return a promise (or use callbacks). 34 | - Ability to **balance task-loads** via distributing executions by tick intervals. 35 | - Ability to **limit total runs** of a task. 36 | - **Stateful tasks**: i.e. ability to auto-stop when all tasks complete. 37 | - `TaskTimer` is also an **`EventEmitter`**. 38 | - **Universal** module. Works in both Node and Browser. 39 | - Small size (4.5kB minified, gzipped). 40 | - Completely **re-written** in **TypeScript**. (version 2.0.0+) 41 | 42 | ## Installation 43 | 44 | ```sh 45 | npm i tasktimer 46 | ``` 47 | 48 | ## Usage 49 | 50 | In Node/CommonJS environments: 51 | ```js 52 | const { TaskTimer } = require('tasktimer'); 53 | ``` 54 | 55 | With transpilers (TypeScript, Babel): 56 | ```js 57 | import { TaskTimer } from 'tasktimer'; 58 | ``` 59 | 60 | In (Modern) Browsers: 61 | ```html 62 | 63 | 66 | ``` 67 | 68 | ### Simplest Example 69 | 70 | ```js 71 | const timer = new TaskTimer(1000); 72 | timer.add(task => console.log(`Current runs: ${task.currentRuns}`)).start(); 73 | ``` 74 | 75 | ### Regular Timer (without Task Management) 76 | 77 | ```js 78 | const timer = new TaskTimer(5000); 79 | timer.on('tick', () => console.log(`Tick count: ${timer.tickCount}`)); 80 | timer.start(); 81 | ``` 82 | 83 | ### Detailed Example 84 | 85 | ```js 86 | // Timer with 1000ms (1 second) base interval resolution. 87 | const timer = new TaskTimer(1000); 88 | // interval can be updated anytime by setting the `timer.interval` property. 89 | 90 | // Add multiple tasks (at once) based on tick intervals. 91 | timer.add([ 92 | { 93 | id: 'task-1', // unique ID of the task 94 | tickInterval: 5, // run every 5 ticks (5 x interval = 5000 ms) 95 | totalRuns: 10, // run 10 times only. (set to 0 for unlimited times) 96 | callback(task) { 97 | // code to be executed on each run 98 | console.log(`${task.id} task has run ${task.currentRuns} times.`); 99 | } 100 | }, 101 | { 102 | id: 'task-2', // unique ID of the task 103 | tickDelay: 1, // 1 tick delay before first run 104 | tickInterval: 10, // run every 10 ticks (10 x interval = 10000 ms) 105 | totalRuns: 2, // run 2 times only. (set to 0 for unlimited times) 106 | callback(task) { 107 | // code to be executed on each run 108 | console.log(`${task.id} task has run ${task.currentRuns} times.`); 109 | } 110 | } 111 | ]); 112 | 113 | // You can also execute some code on each tick... (every 1000 ms) 114 | timer.on('tick', () => { 115 | console.log('tick count: ' + timer.tickCount); 116 | console.log('elapsed time: ' + timer.time.elapsed + ' ms.'); 117 | // stop timer (and all tasks) after 1 hour 118 | if (timer.tickCount >= 3600000) timer.stop(); 119 | }); 120 | 121 | // Start the timer 122 | timer.start(); 123 | ``` 124 | 125 | ## How it works 126 | 127 | - When you create a timer; you set a **time**-interval (e.g. `1000` milliseconds), to be used as **base** resolution (tick) for the tasks. 128 | - Then add task(s) to be executed on **tick**-intervals. 129 | *(e.g. task1 runs on every 10th tick, task2 runs on every 30th)* 130 | - You can optionally define: 131 | - The number of **total runs**, 132 | - An initial **delay**, 133 | - Start/end **dates** for each task... 134 | - In addition to task callbacks; event listeners can be added to execute some other code on each `tick` (base interval) or `task` run, etc... 135 | - You can add, remove, reset, disable individual tasks at any time, without having to stop or re-create the timer. 136 | - Pause and resume the timer at any time; which effects all current tasks. 137 | 138 | ### Documentation 139 | 140 | See [**API reference**][docs] and examples [here][docs]. 141 | 142 | ## Changelog 143 | 144 | See [CHANGELOG.md][changelog]. 145 | *If you're migrating from TaskTimer v1 to v2+, there are various **breaking changes**!..* 146 | 147 | ## Contributing 148 | 149 | Clone original project: 150 | 151 | ```sh 152 | git clone https://github.com/onury/tasktimer.git 153 | ``` 154 | 155 | Install dependencies: 156 | 157 | ```sh 158 | npm install 159 | ``` 160 | 161 | Add tests into [test/node](test/node) and [test/browser](test/browser) and run: 162 | 163 | ```sh 164 | npm run test! # builds and runs tests 165 | npm test # runs tests without building 166 | ``` 167 | 168 | Use included `tslint.json` and `editorconfig` for style and linting. 169 | Travis build should pass, coverage should not degrade. 170 | 171 | ## License 172 | [MIT](LICENSE). 173 | 174 | [docs]:https://onury.io/tasktimer/api 175 | [changelog]:CHANGELOG.md 176 | [how-timers-work]:https://johnresig.com/blog/how-javascript-timers-work/ 177 | [clock-drift]:https://en.wikipedia.org/wiki/Clock_drift 178 | -------------------------------------------------------------------------------- /docma.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": 5, 3 | "jsdoc": { 4 | "encoding": "utf8", 5 | "recurse": false, 6 | "pedantic": false, 7 | "access": [ 8 | "public" 9 | ], 10 | "package": null, 11 | "module": true, 12 | "undocumented": false, 13 | "undescribed": false, 14 | "ignored": false, 15 | "hierarchy": true, 16 | "sort": "grouped", 17 | "relativePath": null, 18 | "filter": "^((?!EventEmitter).)*$", 19 | "allowUnknownTags": true, 20 | "plugins": [], 21 | "includePattern": ".+\\.js(doc|x)?$" 22 | }, 23 | "markdown": { 24 | "gfm": true, 25 | "tables": true, 26 | "breaks": false, 27 | "pedantic": false, 28 | "sanitize": false, 29 | "smartLists": true, 30 | "smartypants": false, 31 | "tasks": true, 32 | "emoji": true 33 | }, 34 | "app": { 35 | "title": "TaskTimer", 36 | "meta": null, 37 | "base": "/tasktimer", 38 | "entrance": "content:guide", 39 | "routing": "path", 40 | "server": "github", 41 | "favicon": "./design/favicon.ico" 42 | }, 43 | "template": { 44 | "path": "zebra", 45 | "options": { 46 | "title": { 47 | "label": "TaskTimer", 48 | "href": "/tasktimer/" 49 | }, 50 | "logo": "img/tasktimer-logo-sm.png", // URL String or { dark: String, light: String } 51 | "sidebar": { 52 | "enabled": true, 53 | "outline": "tree", // "flat" | "tree" 54 | "collapsed": false, 55 | "toolbar": true, 56 | "itemsFolded": false, 57 | "itemsOverflow": "crop", // "crop" | "shrink" 58 | "badges": true, // true | false | 59 | "search": true, 60 | "animations": true 61 | }, 62 | "symbols": { 63 | "autoLink": true, // "internal" | "external" | true (both) 64 | "params": "list", // "list" | "table" 65 | "enums": "list", // "list" | "table" 66 | "props": "list", // "list" | "table" 67 | "meta": false 68 | }, 69 | "contentView": { 70 | "bookmarks": "h1,h2,h3" 71 | }, 72 | "navbar": { 73 | "enabled": true, 74 | "fixed": true, 75 | "dark": false, 76 | "animations": true, 77 | "menu": [ 78 | { 79 | "label": "Guide", 80 | "href": "./" 81 | }, 82 | { 83 | "label": "API Reference", 84 | "href": "./api" 85 | }, 86 | { 87 | "iconClass": "fa-lg fb-github", 88 | "label": "GitHub", 89 | "href": "https://github.com/onury/tasktimer", 90 | "target": "_blank" 91 | } 92 | ] 93 | } 94 | } 95 | }, 96 | "src": [ 97 | "./lib/tasktimer.js", 98 | "./docs/interfaces.jsdoc", 99 | { 100 | "guide": "./README.md" 101 | }, 102 | "./CHANGELOG.md" 103 | ], 104 | "assets": { 105 | "/img": [ 106 | "./design/tasktimer-logo-sm.png" 107 | ] 108 | }, 109 | "dest": "../onury.github.io/tasktimer", 110 | "clean": true 111 | } 112 | -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | // jest-puppeteer config 2 | module.exports = { 3 | // launch: { 4 | // dumpio: true, 5 | // headless: process.env.HEADLESS !== 'false', 6 | // }, 7 | server: { 8 | command: 'node test/_server', 9 | port: 5001 10 | } 11 | }; -------------------------------------------------------------------------------- /jest-puppeteer.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "jest-puppeteer", 3 | "roots": [ 4 | "/lib", 5 | "/test" 6 | ], 7 | "testMatch": [ 8 | "**/test/browser/(*.)?(spec|test).js" 9 | ], 10 | "moduleFileExtensions": [ 11 | "js" 12 | ], 13 | "testPathIgnorePatterns": [ 14 | "/backup/", 15 | "/coverage/", 16 | "/node_modules/", 17 | "/src/" 18 | ], 19 | "collectCoverageFrom": [ 20 | "lib/tasktimer.js", 21 | "!src/**/*" 22 | ], 23 | "coverageDirectory": "./test/coverage" 24 | } -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "roots": [ 4 | "/src", 5 | "/lib", 6 | "/test" 7 | ], 8 | "transform": { 9 | "^.+\\.tsx?$": "ts-jest" 10 | }, 11 | "testMatch": [ 12 | "**/test/node/(*.)?(spec|test).ts" 13 | ], 14 | "moduleFileExtensions": [ 15 | "ts", 16 | "tsx", 17 | "js" 18 | ], 19 | "testPathIgnorePatterns": [ 20 | "/backup/", 21 | "/coverage/" 22 | ], 23 | "collectCoverageFrom": [ 24 | "src/**/*.ts", 25 | "!src/index.ts", 26 | "!src/ITaskOptions.ts", 27 | "!src/ITaskTimerOptions.ts", 28 | "!src/ITaskTimerEvent.ts", 29 | "!src/ITimeInfo.ts", 30 | "!src/TaskCallback.ts" 31 | ], 32 | "coverageDirectory": "./test/coverage" 33 | } -------------------------------------------------------------------------------- /lib/ITaskOptions.d.ts: -------------------------------------------------------------------------------- 1 | import { TaskCallback } from '.'; 2 | /** 3 | * Interface for base task options. 4 | */ 5 | interface ITaskBaseOptions { 6 | /** 7 | * Specifies whether this task is currently enabled. This essentially gives 8 | * you a manual control over execution. The task will always bypass the 9 | * callback while this is set to `false`. 10 | * @type {boolean} 11 | */ 12 | enabled?: boolean; 13 | /** 14 | * Number of ticks to allow before running the task for the first time. 15 | * Default: `0` (ticks) 16 | * @type {number} 17 | */ 18 | tickDelay?: number; 19 | /** 20 | * Tick interval that the task should be run on. The unit is "ticks" (not 21 | * milliseconds). For instance, if the timer interval is `1000` 22 | * milliseconds, and we add a task with `5` tick intervals. The task will 23 | * run on every `5` seconds. Default: `1` (minimum tick interval) 24 | * @type {number} 25 | */ 26 | tickInterval?: number; 27 | /** 28 | * Total number of times the task should be run. `0` or `null` means 29 | * unlimited until `stopDate` is reached or the timer has stopped. If 30 | * `stopDate` is reached before `totalRuns` is fulfilled, task will still 31 | * be considered completed and will not be executed any more. Default: 32 | * `null` 33 | * @type {number} 34 | */ 35 | totalRuns?: number; 36 | /** 37 | * Indicates the initial date and time to start executing the task on given 38 | * interval. If omitted, task will be executed on defined tick interval, 39 | * right after the timer starts. 40 | * @type {number|Date} 41 | */ 42 | startDate?: number | Date; 43 | /** 44 | * Indicates the final date and time to execute the task. If `totalRuns` is 45 | * set and it's reached before this date; task will be considered completed 46 | * and will not be executed any more. If `stopDate` is omitted, task will 47 | * be executed until `totalRuns` is fulfilled or timer is stopped. 48 | * @type {number|Date} 49 | */ 50 | stopDate?: number | Date; 51 | /** 52 | * Whether to wrap callback in a `setImmediate()` call before executing. 53 | * This can be useful if the task is not doing any I/O or using any JS 54 | * timers but synchronously blocking the event loop. Default: `false` 55 | * @type {boolean} 56 | */ 57 | immediate?: boolean; 58 | /** 59 | * Specifies whether to remove the task (to free up memory) when task has 60 | * completed its executions (runs). For this to take affect, the task 61 | * should have `totalRuns` and/or `stopDate` configured. Default: `false` 62 | * @type {boolean} 63 | */ 64 | removeOnCompleted?: boolean; 65 | /** 66 | * The callback function of the task to be executed on each run. The task 67 | * itself is passed to this callback, as the first argument. If you're 68 | * defining an async task; either return a `Promise` or call `done()` 69 | * function which is passed as the second argument to the callback. 70 | * @type {TaskCallback} 71 | * 72 | * @example Using done() function 73 | * timer.add({ 74 | * callback(task, done) { 75 | * fs.readFile(filePath, () => done()); 76 | * } 77 | * }); 78 | * @example Returning a Promise() 79 | * timer.add({ 80 | * callback(task) { 81 | * return readFileAsync().then(result => { 82 | * // do some stuff... 83 | * }); 84 | * } 85 | * }); 86 | */ 87 | callback: TaskCallback; 88 | } 89 | /** 90 | * Interface for task options. 91 | * @extends ITaskBaseOptions 92 | */ 93 | interface ITaskOptions extends ITaskBaseOptions { 94 | /** 95 | * Unique ID of the task. Required if creating a `Task` instance directly. 96 | * Can be omitted if creating via `TaskTimer#add()` method. In this case, a 97 | * unique ID will be auto-generated in `task-{n}` format. 98 | * @type {string} 99 | */ 100 | id?: string; 101 | } 102 | export { ITaskBaseOptions, ITaskOptions }; 103 | -------------------------------------------------------------------------------- /lib/ITaskTimerEvent.d.ts: -------------------------------------------------------------------------------- 1 | import { TaskTimer } from '.'; 2 | /** 3 | * Interface for time information for the latest run of the timer. 4 | */ 5 | interface ITaskTimerEvent { 6 | /** 7 | * Indicates the name of the event. 8 | * @type {TaskTimer.Event} 9 | */ 10 | name: TaskTimer.Event; 11 | /** 12 | * Indicates the source object fired this event. 13 | * @type {any} 14 | */ 15 | source: any; 16 | /** 17 | * Any object passed to the event emitter. This is generally a `Task` 18 | * instance if set. 19 | * @type {any} 20 | */ 21 | data?: any; 22 | /** 23 | * Any `Error` instance passed to the event emitter. This is generally a 24 | * task error instance if set. 25 | * @type {any} 26 | */ 27 | error?: Error; 28 | } 29 | export { ITaskTimerEvent }; 30 | -------------------------------------------------------------------------------- /lib/ITaskTimerOptions.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for `TaskTimer` options. 3 | */ 4 | interface ITaskTimerOptions { 5 | /** 6 | * Timer interval in milliseconds. Since the tasks run on ticks instead of 7 | * millisecond intervals; this value operates as the base resolution for 8 | * all tasks. If you are running heavy tasks, lower interval requires 9 | * higher CPU power. This value can be updated any time by setting the 10 | * `interval` property on the `TaskTimer` instance. Default: `1000` 11 | * (milliseconds) 12 | * @type {number} 13 | */ 14 | interval?: number; 15 | /** 16 | * Specifies whether the timer should auto-adjust the delay between ticks 17 | * if it's off due to task/CPU loads or clock-drifts. Note that precision 18 | * will be as high as possible but it still can be off by a few 19 | * milliseconds; depending on the CPU. Default: `true` 20 | * @type {Boolean} 21 | */ 22 | precision?: boolean; 23 | /** 24 | * Specifies whether to automatically stop the timer when all tasks are 25 | * completed. For this to take affect, all added tasks should have 26 | * `totalRuns` and/or `stopDate` configured. Default: `false` 27 | * @type {boolean} 28 | */ 29 | stopOnCompleted?: boolean; 30 | } 31 | export { ITaskTimerOptions }; 32 | -------------------------------------------------------------------------------- /lib/ITimeInfo.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores time information for a timer or task. 3 | */ 4 | interface ITimeInfo { 5 | /** 6 | * Indicates the start time of a timer or task. 7 | * @type {number} 8 | */ 9 | started: number; 10 | /** 11 | * Indicates the stop time of a timer or task. (`0` if still running.) 12 | * @type {number} 13 | */ 14 | stopped: number; 15 | /** 16 | * Indicates the the elapsed time of a timer or task, in milliseconds. 17 | * @type {number} 18 | */ 19 | elapsed: number; 20 | } 21 | export { ITimeInfo }; 22 | -------------------------------------------------------------------------------- /lib/Task.d.ts: -------------------------------------------------------------------------------- 1 | import { ITaskBaseOptions, ITaskOptions, ITimeInfo, TaskCallback } from '.'; 2 | /** 3 | * Represents the class that holds the configurations and the callback function 4 | * required to run a task. 5 | * @class 6 | */ 7 | declare class Task { 8 | /** 9 | * @private 10 | */ 11 | private _timer; 12 | /** 13 | * @private 14 | */ 15 | private _markedCompleted; 16 | /** 17 | * @private 18 | */ 19 | private _; 20 | /** 21 | * Initializes a new instance of `Task` class. 22 | * @constructor 23 | * @param {ITaskOptions} options Task options. 24 | */ 25 | constructor(options: ITaskOptions); 26 | /** 27 | * Gets the unique ID of the task. 28 | * @name Task#id 29 | * @type {string} 30 | * @readonly 31 | */ 32 | readonly id: string; 33 | /** 34 | * Specifies whether this task is currently enabled. This essentially gives 35 | * you a manual control over execution. The task will always bypass the 36 | * callback while this is set to `false`. 37 | * @name Task#enabled 38 | * @type {boolean} 39 | */ 40 | enabled: boolean; 41 | /** 42 | * Gets or sets the number of ticks to allow before running the task for 43 | * the first time. 44 | * @name Task#tickDelay 45 | * @type {number} 46 | */ 47 | tickDelay: number; 48 | /** 49 | * Gets or sets the tick interval that the task should be run on. The unit 50 | * is "ticks" (not milliseconds). For instance, if the timer interval is 51 | * `1000` milliseconds, and we add a task with `5` tick intervals. The task 52 | * will run on every `5` seconds. 53 | * @name Task#tickInterval 54 | * @type {number} 55 | */ 56 | tickInterval: number; 57 | /** 58 | * Gets or sets the total number of times the task should be run. `0` or 59 | * `null` means unlimited (until the timer has stopped). 60 | * @name Task#totalRuns 61 | * @type {number} 62 | */ 63 | totalRuns: number; 64 | /** 65 | * Specifies whether to wrap callback in a `setImmediate()` call before 66 | * executing. This can be useful if the task is not doing any I/O or using 67 | * any JS timers but synchronously blocking the event loop. 68 | * @name Task#immediate 69 | * @type {boolean} 70 | */ 71 | immediate: boolean; 72 | /** 73 | * Gets the number of times, this task has been run. 74 | * @name Task#currentRuns 75 | * @type {number} 76 | * @readonly 77 | */ 78 | readonly currentRuns: number; 79 | /** 80 | * Gets time information for the lifetime of a task. 81 | * `#time.started` indicates the first execution time of a task. 82 | * `#time.stopped` indicates the last execution time of a task. (`0` if still running.) 83 | * `#time.elapsed` indicates the total lifetime of a task. 84 | * @name Task#time 85 | * @type {ITimeInfo} 86 | * @readonly 87 | */ 88 | readonly time: ITimeInfo; 89 | /** 90 | * Gets the callback function to be executed on each run. 91 | * @name Task#callback 92 | * @type {TaskCallback} 93 | * @readonly 94 | */ 95 | readonly callback: TaskCallback; 96 | /** 97 | * Gets or sets whether to remove the task (to free up memory) when task 98 | * has completed its executions (runs). For this to take affect, the task 99 | * should have `totalRuns` and/or `stopDate` configured. 100 | * @name Task#removeOnCompleted 101 | * @type {boolean} 102 | */ 103 | removeOnCompleted: boolean; 104 | /** 105 | * Specifies whether the task has completed all runs (executions) or 106 | * `stopDate` is reached. Note that if both `totalRuns` and `stopDate` are 107 | * omitted, this will never return `true`; since the task has no execution 108 | * limit set. 109 | * @name Task#completed 110 | * @type {boolean} 111 | * @readonly 112 | */ 113 | readonly completed: boolean; 114 | /** 115 | * Specifies whether the task can run on the current tick of the timer. 116 | * @private 117 | * @name Task#canRunOnTick 118 | * @type {boolean} 119 | * @readonly 120 | */ 121 | readonly canRunOnTick: boolean; 122 | /** 123 | * Resets the current number of runs. This will keep the task running for 124 | * the same amount of `tickIntervals` initially configured. 125 | * @memberof Task 126 | * @chainable 127 | * 128 | * @param {ITaskBaseOptions} [options] If set, this will also re-configure the task. 129 | * 130 | * @returns {Task} 131 | */ 132 | reset(options?: ITaskBaseOptions): Task; 133 | /** 134 | * Serialization to JSON. 135 | * 136 | * Never return string from `toJSON()`. It should return an object. 137 | * @private 138 | */ 139 | toJSON(): any; 140 | /** 141 | * Set reference to timer itself. 142 | * Only called by `TaskTimer`. 143 | * @private 144 | */ 145 | private _setTimer; 146 | /** 147 | * @private 148 | */ 149 | private _emit; 150 | /** 151 | * `TaskTimer` should be informed if this task is completed. But execution 152 | * should be finished. So we do this within the `done()` function. 153 | * @private 154 | */ 155 | private _done; 156 | /** 157 | * @private 158 | */ 159 | private _execCallback; 160 | /** 161 | * Only used by `TaskTimer`. 162 | * @private 163 | */ 164 | private _run; 165 | /** 166 | * @private 167 | */ 168 | private _init; 169 | } 170 | export { Task }; 171 | -------------------------------------------------------------------------------- /lib/TaskCallback.d.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '.'; 2 | /** 3 | * Defines a callback function for a task to be executed on each run. The task 4 | * itself is passed to this callback, as the first argument. If you're 5 | * defining an async task; either return a `Promise` or call `done()` 6 | * function which is passed as the second argument to the callback. 7 | */ 8 | export declare type TaskCallback = (task: Task, done?: Function) => void; 9 | -------------------------------------------------------------------------------- /lib/TaskTimer.d.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'eventemitter3'; 2 | import { ITaskOptions, ITaskTimerOptions, ITimeInfo, Task as TTask, TaskCallback } from '.'; 3 | /** 4 | * TaskTimer • https://github.com/onury/tasktimer 5 | * @license MIT 6 | * @copyright 2019, Onur Yıldırım 7 | */ 8 | /** 9 | * Calls each of the listeners registered for a given event name. 10 | * @name TaskTimer#emit 11 | * @function 12 | * 13 | * @param {TaskTimer.Event} eventName - The name of the event to be emitted. 14 | * @param {any} [data] - Data to be passed to event listeners. 15 | * 16 | * @returns {Boolean} - `true` if the event had listeners, else `false`. 17 | */ 18 | /** 19 | * Return an array listing the events for which the emitter has registered 20 | * listeners. 21 | * @name TaskTimer#eventNames 22 | * @function 23 | * 24 | * @returns {Array} - List of event names. 25 | */ 26 | /** 27 | * Adds the listener function to the end of the listeners array for the event 28 | * named `eventName`. No checks are made to see if the listener has already 29 | * been added. Multiple calls passing the same combination of `eventName` and 30 | * `listener` will result in the listener being added, and called, multiple 31 | * times. 32 | * @name TaskTimer#on 33 | * @function 34 | * @alias TaskTimer#addListener 35 | * @chainable 36 | * 37 | * @param {TaskTimer.Event} eventName - The name of the event to be added. 38 | * @param {Function} listener - The callback function to be invoked per event. 39 | * @param {*} [context=this] - The context to invoke the listener with. 40 | * 41 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 42 | * 43 | * @example 44 | * const timer = new TaskTimer(1000); 45 | * // add a listener to be invoked when timer has stopped. 46 | * timer.on(TaskTimer.Event.STOPPED, () => { 47 | * console.log('Timer has stopped!'); 48 | * }); 49 | * timer.start(); 50 | */ 51 | /** 52 | * Adds a one time listener function for the event named `eventName`. The next 53 | * time `eventName` is triggered, this `listener` is removed and then invoked. 54 | * @name TaskTimer#once 55 | * @function 56 | * @chainable 57 | * 58 | * @param {TaskTimer.Event} eventName - The name of the event to be added. 59 | * @param {Function} listener - The callback function to be invoked per event. 60 | * @param {*} [context=this] - The context to invoke the listener with. 61 | * 62 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 63 | */ 64 | /** 65 | * Removes the specified `listener` from the listener array for the event 66 | * named `eventName`. 67 | * @name TaskTimer#off 68 | * @function 69 | * @alias TaskTimer#removeListener 70 | * @chainable 71 | * 72 | * @param {TaskTimer.Event} eventName - The name of the event to be removed. 73 | * @param {Function} listener - The callback function to be invoked per event. 74 | * @param {*} [context=this] - Only remove the listeners that have this context. 75 | * @param {Boolean} [once=false] - Only remove one-time listeners. 76 | * 77 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 78 | */ 79 | /** 80 | * Gets the number of listeners listening to a given event. 81 | * @name TaskTimer#listenerCount 82 | * @function 83 | * 84 | * @param {TaskTimer.Event} eventName - The name of the event. 85 | * 86 | * @returns {Number} - The number of listeners. 87 | */ 88 | /** 89 | * Gets the listeners registered for a given event. 90 | * @name TaskTimer#listeners 91 | * @function 92 | * 93 | * @param {TaskTimer.Event} eventName - The name of the event. 94 | * 95 | * @returns {Array} - The registered listeners. 96 | */ 97 | /** 98 | * Removes all listeners, or those of the specified `eventName`. 99 | * @name TaskTimer#removeAllListeners 100 | * @function 101 | * @chainable 102 | * 103 | * @param {TaskTimer.Event} [eventName] - The name of the event to be removed. 104 | * 105 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 106 | */ 107 | /** 108 | * A timer utility for running periodic tasks on the given interval ticks. This 109 | * is useful when you want to run or schedule multiple tasks on a single timer 110 | * instance. 111 | * 112 | * This class extends `EventEmitter3` which is an `EventEmitter` implementation 113 | * for both Node and browser. For detailed information, refer to Node.js 114 | * documentation. 115 | * @class 116 | * @global 117 | * 118 | * @extends EventEmitter 119 | * 120 | * @see 121 | * {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter} 122 | */ 123 | declare class TaskTimer extends EventEmitter { 124 | /** 125 | * Inner storage for Tasktimer. 126 | * @private 127 | */ 128 | private _; 129 | /** 130 | * setTimeout reference used by the timmer. 131 | * @private 132 | */ 133 | private _timeoutRef; 134 | /** 135 | * setImmediate reference used by the timer. 136 | * @private 137 | */ 138 | private _immediateRef; 139 | /** 140 | * Timer run count storage. 141 | * @private 142 | */ 143 | private _runCount; 144 | /** 145 | * Constructs a new `TaskTimer` instance with the given time interval (in 146 | * milliseconds). 147 | * @constructor 148 | * 149 | * @param {ITaskTimerOptions|number} [options] - Either TaskTimer options 150 | * or a base interval (in milliseconds). Since the tasks run on ticks 151 | * instead of millisecond intervals; this value operates as the base 152 | * resolution for all tasks. If you are running heavy tasks, lower interval 153 | * requires higher CPU power. This value can be updated any time by setting 154 | * the `interval` property on the instance. 155 | * 156 | * @example 157 | * const timer = new TaskTimer(1000); // milliseconds 158 | * // Execute some code on each tick... 159 | * timer.on('tick', () => { 160 | * console.log('tick count: ' + timer.tickCount); 161 | * console.log('elapsed time: ' + timer.time.elapsed + ' ms.'); 162 | * }); 163 | * // add a task named 'heartbeat' that runs every 5 ticks and a total of 10 times. 164 | * const task1 = { 165 | * id: 'heartbeat', 166 | * tickDelay: 20, // ticks (to wait before first run) 167 | * tickInterval: 5, // ticks (interval) 168 | * totalRuns: 10, // times to run 169 | * callback(task) { // can also be an async function, returning a promise 170 | * console.log(task.id + ' task has run ' + task.currentRuns + ' times.'); 171 | * } 172 | * }; 173 | * timer.add(task1).start(); 174 | */ 175 | constructor(options?: ITaskTimerOptions | number); 176 | /** 177 | * Gets or sets the base timer interval in milliseconds. 178 | * 179 | * Since the tasks run on ticks instead of millisecond intervals; this 180 | * value operates as the base resolution for all tasks. If you are running 181 | * heavy tasks, lower interval requires higher CPU power. This value can be 182 | * updated any time. 183 | * 184 | * @name TaskTimer#interval 185 | * @type {number} 186 | */ 187 | interval: number; 188 | /** 189 | * Gets or sets whether timer precision enabled. 190 | * 191 | * Because of the single-threaded, asynchronous nature of JavaScript, each 192 | * execution takes a piece of CPU time, and the time they have to wait will 193 | * vary, depending on the load. This creates a latency and cumulative 194 | * difference in asynchronous timers; that gradually increase the 195 | * inacuraccy. `TaskTimer` overcomes this problem as much as possible: 196 | * 197 | *
  • The delay between each tick is auto-adjusted when it's off 198 | * due to task/CPU loads or clock drifts.
  • 199 | *
  • In Node.js, `TaskTimer` also makes use of `process.hrtime()` 200 | * high-resolution real-time. The time is relative to an arbitrary 201 | * time in the past (not related to the time of day) and therefore not 202 | * subject to clock drifts.
  • 203 | *
  • The timer may hit a synchronous / blocking task; or detect significant 204 | * time drift (longer than the base interval) due to JS event queue, which 205 | * cannot be recovered by simply adjusting the next delay. In this case, right 206 | * from the next tick onward; it will auto-recover as much as possible by 207 | * running "immediate" tasks until it reaches the proper time vs tick/run 208 | * balance.
  • 209 | * 210 | *
    Note that precision will be as high as possible but it still 211 | * can be off by a few milliseconds; depending on the CPU or the load. 212 | *
    213 | * @name TaskTimer#precision 214 | * @type {boolean} 215 | */ 216 | precision: boolean; 217 | /** 218 | * Gets or sets whether the timer should automatically stop when all tasks 219 | * are completed. For this to take affect, all added tasks should have 220 | * `totalRuns` and/or `stopDate` configured. This option can be set/changed 221 | * at any time. 222 | * @name TaskTimer#stopOnCompleted 223 | * @type {boolean} 224 | */ 225 | stopOnCompleted: boolean; 226 | /** 227 | * Gets the current state of the timer. 228 | * For possible values, see `TaskTimer.State` enumeration. 229 | * @name TaskTimer#state 230 | * @type {TaskTimer.State} 231 | * @readonly 232 | */ 233 | readonly state: TaskTimer.State; 234 | /** 235 | * Gets time information for the latest run of the timer. 236 | * `#time.started` indicates the start time of the timer. 237 | * `#time.stopped` indicates the stop time of the timer. (`0` if still running.) 238 | * `#time.elapsed` indicates the elapsed time of the timer. 239 | * @name TaskTimer#time 240 | * @type {ITimeInfo} 241 | * @readonly 242 | */ 243 | readonly time: ITimeInfo; 244 | /** 245 | * Gets the current tick count for the latest run of the timer. 246 | * This value will be reset to `0` when the timer is stopped or reset. 247 | * @name TaskTimer#tickCount 248 | * @type {Number} 249 | * @readonly 250 | */ 251 | readonly tickCount: number; 252 | /** 253 | * Gets the current task count. Tasks remain even after the timer is 254 | * stopped. But they will be removed if the timer is reset. 255 | * @name TaskTimer#taskCount 256 | * @type {Number} 257 | * @readonly 258 | */ 259 | readonly taskCount: number; 260 | /** 261 | * Gets the total number of all task executions (runs). 262 | * @name TaskTimer#taskRunCount 263 | * @type {Number} 264 | * @readonly 265 | */ 266 | readonly taskRunCount: number; 267 | /** 268 | * Gets the total number of timer runs, including resumed runs. 269 | * @name TaskTimer#runCount 270 | * @type {Number} 271 | * @readonly 272 | */ 273 | readonly runCount: number; 274 | /** 275 | * Gets the task with the given ID. 276 | * @memberof TaskTimer 277 | * 278 | * @param {String} id - ID of the task. 279 | * 280 | * @returns {Task} 281 | */ 282 | get(id: string): TTask; 283 | /** 284 | * Adds a collection of new tasks for the timer. 285 | * @memberof TaskTimer 286 | * @chainable 287 | * 288 | * @param {Task|ITaskOptions|TaskCallback|Array} task - Either a 289 | * single task, task options object or the callback function; or a mixture 290 | * of these as an array. 291 | * 292 | * @returns {TaskTimer} 293 | * 294 | * @throws {Error} - If a task callback is not set or a task with the given 295 | * name already exists. 296 | */ 297 | add(task: TTask | ITaskOptions | TaskCallback | Array): TaskTimer; 298 | /** 299 | * Removes the task by the given name. 300 | * @memberof TaskTimer 301 | * @chainable 302 | * 303 | * @param {string|Task} task - Task to be removed. Either pass the 304 | * name or the task itself. 305 | * 306 | * @returns {TaskTimer} 307 | * 308 | * @throws {Error} - If a task with the given name does not exist. 309 | */ 310 | remove(task: string | TTask): TaskTimer; 311 | /** 312 | * Starts the timer and puts the timer in `RUNNING` state. If it's already 313 | * running, this will reset the start/stop time and tick count, but will not 314 | * reset (or remove) existing tasks. 315 | * @memberof TaskTimer 316 | * @chainable 317 | * 318 | * @returns {TaskTimer} 319 | */ 320 | start(): TaskTimer; 321 | /** 322 | * Pauses the timer, puts the timer in `PAUSED` state and all tasks on hold. 323 | * @memberof TaskTimer 324 | * @chainable 325 | * 326 | * @returns {TaskTimer} 327 | */ 328 | pause(): TaskTimer; 329 | /** 330 | * Resumes the timer and puts the timer in `RUNNING` state; if previuosly 331 | * paused. In this state, all existing tasks are resumed. 332 | * @memberof TaskTimer 333 | * @chainable 334 | * 335 | * @returns {TaskTimer} 336 | */ 337 | resume(): TaskTimer; 338 | /** 339 | * Stops the timer and puts the timer in `STOPPED` state. In this state, all 340 | * existing tasks are stopped and no values or tasks are reset until 341 | * re-started or explicitly calling reset. 342 | * @memberof TaskTimer 343 | * @chainable 344 | * 345 | * @returns {TaskTimer} 346 | */ 347 | stop(): TaskTimer; 348 | /** 349 | * Stops the timer and puts the timer in `IDLE` state. 350 | * This will reset the ticks and removes all tasks silently; meaning no 351 | * other events will be emitted such as `"taskRemoved"`. 352 | * @memberof TaskTimer 353 | * @chainable 354 | * 355 | * @returns {TaskTimer} 356 | */ 357 | reset(): TaskTimer; 358 | /** 359 | * @private 360 | */ 361 | private _emit; 362 | /** 363 | * Adds a new task for the timer. 364 | * @private 365 | * 366 | * @param {Task|ITaskOptions|TaskCallback} options - Either a task instance, 367 | * task options object or the callback function to be executed on tick 368 | * intervals. 369 | * 370 | * @returns {TaskTimer} 371 | * 372 | * @throws {Error} - If the task callback is not set or a task with the 373 | * given name already exists. 374 | */ 375 | private _add; 376 | /** 377 | * Stops the timer. 378 | * @private 379 | */ 380 | private _stop; 381 | /** 382 | * Resets the timer. 383 | * @private 384 | */ 385 | private _reset; 386 | /** 387 | * Called (by Task instance) when it has completed all of its runs. 388 | * @private 389 | */ 390 | private _taskCompleted; 391 | /** 392 | * Handler to be executed on each tick. 393 | * @private 394 | */ 395 | private _tick; 396 | /** 397 | * Marks the resume (or start) time in milliseconds or high-resolution time 398 | * if available. 399 | * @private 400 | */ 401 | private _markTime; 402 | /** 403 | * Gets the time difference in milliseconds sinct the last resume or start 404 | * time. 405 | * @private 406 | */ 407 | private _getTimeDiff; 408 | /** 409 | * Runs the timer. 410 | * @private 411 | */ 412 | private _run; 413 | /** 414 | * Gets a unique task ID. 415 | * @private 416 | */ 417 | private _getUniqueTaskID; 418 | } 419 | /** @private */ 420 | declare namespace TaskTimer { 421 | /** 422 | * Represents the class that holds the configurations and the callback function 423 | * required to run a task. See {@link api/#Task|class information}. 424 | * @name TaskTimer.Task 425 | * @class 426 | */ 427 | const Task: typeof TTask; 428 | /** 429 | * Enumerates `TaskTimer` states. 430 | * @memberof TaskTimer 431 | * @enum {String} 432 | * @readonly 433 | */ 434 | enum State { 435 | /** 436 | * Indicates that the timer is in `idle` state. 437 | * This is the initial state when the `TaskTimer` instance is first created. 438 | * Also when an existing timer is reset, it will be `idle`. 439 | * @type {String} 440 | */ 441 | IDLE = "idle", 442 | /** 443 | * Indicates that the timer is in `running` state; such as when the timer is 444 | * started or resumed. 445 | * @type {String} 446 | */ 447 | RUNNING = "running", 448 | /** 449 | * Indicates that the timer is in `paused` state. 450 | * @type {String} 451 | */ 452 | PAUSED = "paused", 453 | /** 454 | * Indicates that the timer is in `stopped` state. 455 | * @type {String} 456 | */ 457 | STOPPED = "stopped" 458 | } 459 | /** 460 | * Enumerates the `TaskTimer` event types. 461 | * @memberof TaskTimer 462 | * @enum {String} 463 | * @readonly 464 | */ 465 | enum Event { 466 | /** 467 | * Emitted on each tick (interval) of `TaskTimer`. 468 | * @type {String} 469 | */ 470 | TICK = "tick", 471 | /** 472 | * Emitted when the timer is put in `RUNNING` state; such as when the timer is 473 | * started. 474 | * @type {String} 475 | */ 476 | STARTED = "started", 477 | /** 478 | * Emitted when the timer is put in `RUNNING` state; such as when the timer is 479 | * resumed. 480 | * @type {String} 481 | */ 482 | RESUMED = "resumed", 483 | /** 484 | * Emitted when the timer is put in `PAUSED` state. 485 | * @type {String} 486 | */ 487 | PAUSED = "paused", 488 | /** 489 | * Emitted when the timer is put in `STOPPED` state. 490 | * @type {String} 491 | */ 492 | STOPPED = "stopped", 493 | /** 494 | * Emitted when the timer is reset. 495 | * @type {String} 496 | */ 497 | RESET = "reset", 498 | /** 499 | * Emitted when a task is executed. 500 | * @type {String} 501 | */ 502 | TASK = "task", 503 | /** 504 | * Emitted when a task is added to `TaskTimer` instance. 505 | * @type {String} 506 | */ 507 | TASK_ADDED = "taskAdded", 508 | /** 509 | * Emitted when a task is removed from `TaskTimer` instance. 510 | * Note that this will not be emitted when `.reset()` is called; which 511 | * removes all tasks silently. 512 | * @type {String} 513 | */ 514 | TASK_REMOVED = "taskRemoved", 515 | /** 516 | * Emitted when a task has completed all of its executions (runs) 517 | * or reached its stopping date/time (if set). Note that this event 518 | * will only be fired if the tasks has a `totalRuns` limit or a 519 | * `stopDate` value set. 520 | * @type {String} 521 | */ 522 | TASK_COMPLETED = "taskCompleted", 523 | /** 524 | * Emitted when a task produces an error on its execution. 525 | * @type {String} 526 | */ 527 | TASK_ERROR = "taskError", 528 | /** 529 | * Emitted when all tasks have completed all of their executions (runs) 530 | * or reached their stopping date/time (if set). Note that this event 531 | * will only be fired if all tasks have a `totalRuns` limit or a 532 | * `stopDate` value set. 533 | * @type {String} 534 | */ 535 | COMPLETED = "completed" 536 | } 537 | } 538 | export { TaskTimer }; 539 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './ITaskOptions'; 2 | export * from './ITaskTimerOptions'; 3 | export * from './ITaskTimerEvent'; 4 | export * from './ITimeInfo'; 5 | export * from './Task'; 6 | export * from './TaskCallback'; 7 | export * from './TaskTimer'; 8 | -------------------------------------------------------------------------------- /lib/tasktimer.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("tasktimer",[],e):"object"==typeof exports?exports.tasktimer=e():t.tasktimer=e()}(this,function(){return r={},i.m=n=[function(t,n,e){"use strict";function r(t){for(var e in t)n.hasOwnProperty(e)||(n[e]=t[e])}Object.defineProperty(n,"__esModule",{value:!0}),r(e(2)),r(e(3))},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=Object.prototype,i="function"==typeof setImmediate&&"object"==typeof process&&"function"==typeof process.hrtime,o={NODE:i,BROWSER:!i,type:function(t){return r.toString.call(t).match(/\s(\w+)/i)[1].toLowerCase()},isset:function(t){return null!=t},ensureArray:function(t){return o.isset(t)?Array.isArray(t)?t:[t]:[]},getNumber:function(t,e,n){return"number"==typeof t?t=this.totalRuns||this._.stopDate&&Date.now()>=this._.stopDate)},enumerable:!0,configurable:!0}),Object.defineProperty(a.prototype,"canRunOnTick",{get:function(){if(this._markedCompleted)return!1;var t=this._.startDate?Math.ceil((Date.now()-Number(this._.startDate))/this._timer.interval):this._timer.tickCount,e=!this._.startDate||Date.now()>=this._.startDate,n=t>this.tickDelay&&(t-this.tickDelay)%this.tickInterval==0;return Boolean(e&&n)},enumerable:!0,configurable:!0}),a.prototype.reset=function(t){if(this._.currentRuns=0,t){var e=t.id;if(e&&e!==this.id)throw new Error("Cannot change ID of a task.");t.id=this.id,this._init(t)}return this},a.prototype.toJSON=function(){var t=r({},this._);return delete t.callback,t},a.prototype._setTimer=function(t){this._timer=t},a.prototype._emit=function(t,e){var n={name:t,source:this};e instanceof Error?n.error=e:n.data=e,this._timer.emit(t,n)},a.prototype._done=function(){this.completed&&(this._markedCompleted=!0,this._.timeOnLastRun=Date.now(),this._timer._taskCompleted(this))},a.prototype._execCallback=function(){var e=this;try{var t=this.callback.apply(this,[this,function(){return e._done()}]);2<=this.callback.length||(o.utils.isPromise(t)?t.then(function(){e._done()}).catch(function(t){e._emit(i.TaskTimer.Event.TASK_ERROR,t)}):this._done())}catch(t){this._emit(i.TaskTimer.Event.TASK_ERROR,t)}},a.prototype._run=function(t){var e=this;this.enabled&&!this._markedCompleted&&(0===this.currentRuns&&(this._.timeOnFirstRun=Date.now()),this._.currentRuns++,t(),this.immediate?o.utils.setImmediate(function(){return e._execCallback()}):this._execCallback())},a.prototype._init=function(t){if(!t||!t.id)throw new Error("A unique task ID is required.");if("function"!=typeof t.callback)throw new Error("A callback function is required for a task to run.");var e=t.startDate,n=t.stopDate;if(e&&n&&n<=e)throw new Error("Task start date cannot be the same or after stop date.");this._markedCompleted=!1,this._=r({currentRuns:0},s),this._.id=String(t.id),this._.callback=t.callback,this._.startDate=t.startDate||null,this._.stopDate=t.stopDate||null,this.enabled=t.enabled,this.tickDelay=t.tickDelay,this.tickInterval=t.tickInterval,this.totalRuns=t.totalRuns,this.immediate=t.immediate,this.removeOnCompleted=t.removeOnCompleted},a);function a(t){this._init(t)}e.Task=u},function(t,e,n){"use strict";var r,i=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(t,e)},function(t,e){function n(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)});Object.defineProperty(e,"__esModule",{value:!0});var o,s,u,a,c=n(4),p=n(0),l=n(1),f=Object.freeze({interval:1e3,precision:!0,stopOnCompleted:!1}),h=(o=c.EventEmitter,i(m,o),Object.defineProperty(m.prototype,"interval",{get:function(){return this._.opts.interval},set:function(t){this._.opts.interval=l.utils.getNumber(t,20,f.interval)},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"precision",{get:function(){return this._.opts.precision},set:function(t){this._.opts.precision=l.utils.getBool(t,f.precision)},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"stopOnCompleted",{get:function(){return this._.opts.stopOnCompleted},set:function(t){this._.opts.stopOnCompleted=l.utils.getBool(t,f.stopOnCompleted)},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"state",{get:function(){return this._.state},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"time",{get:function(){var t=this._,e=t.startTime,n=t.stopTime,r={started:e,stopped:n,elapsed:0};if(e){var i=this.state!==m.State.STOPPED?Date.now():n;r.elapsed=i-e}return Object.freeze(r)},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"tickCount",{get:function(){return this._.tickCount},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"taskCount",{get:function(){return Object.keys(this._.tasks).length},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"taskRunCount",{get:function(){return this._.taskRunCount},enumerable:!0,configurable:!0}),Object.defineProperty(m.prototype,"runCount",{get:function(){return this._runCount},enumerable:!0,configurable:!0}),m.prototype.get=function(t){return this._.tasks[t]||null},m.prototype.add=function(t){var e=this;if(!l.utils.isset(t))throw new Error("Either a task, task options or a callback is required.");return l.utils.ensureArray(t).forEach(function(t){return e._add(t)}),this},m.prototype.remove=function(t){var e="string"==typeof t?t:t.id;if(t=this.get(e),!e||!t)throw new Error("No tasks exist with ID: '"+e+"'.");return t.completed&&0this._.tickCountAfterResume)return void(this._immediateRef=l.utils.setImmediate(function(){return t._tick()}));e-=n%e}this._timeoutRef=setTimeout(function(){return t._tick()},e)}},m.prototype._getUniqueTaskID=function(){for(var t,e=this.taskCount;!t||this.get(t);)t="task"+ ++e;return t},m);function m(t){var e=o.call(this)||this;e._timeoutRef=null,e._immediateRef=null,e._runCount=0,e._reset(),e._.opts={};var n="number"==typeof t?{interval:t}:t||{};return e.interval=n.interval,e.precision=n.precision,e.stopOnCompleted=n.stopOnCompleted,e}e.TaskTimer=h,(s=h=h||{}).Task=p.Task,(u=s.State||(s.State={})).IDLE="idle",u.RUNNING="running",u.PAUSED="paused",u.STOPPED="stopped",(a=s.Event||(s.Event={})).TICK="tick",a.STARTED="started",a.RESUMED="resumed",a.PAUSED="paused",a.STOPPED="stopped",a.RESET="reset",a.TASK="task",a.TASK_ADDED="taskAdded",a.TASK_REMOVED="taskRemoved",a.TASK_COMPLETED="taskCompleted",a.TASK_ERROR="taskError",a.COMPLETED="completed",e.TaskTimer=h},function(t,e,n){"use strict";var r=Object.prototype.hasOwnProperty,h="~";function i(){}function u(t,e,n){this.fn=t,this.context=e,this.once=n||!1}function o(t,e,n,r,i){if("function"!=typeof n)throw new TypeError("The listener must be a function");var o=new u(n,r||t,i),s=h?h+e:e;return t._events[s]?t._events[s].fn?t._events[s]=[t._events[s],o]:t._events[s].push(o):(t._events[s]=o,t._eventsCount++),t}function c(t,e){0==--t._eventsCount?t._events=new i:delete t._events[e]}function s(){this._events=new i,this._eventsCount=0}Object.create&&(i.prototype=Object.create(null),(new i).__proto__||(h=!1)),s.prototype.eventNames=function(){var t,e,n=[];if(0===this._eventsCount)return n;for(e in t=this._events)r.call(t,e)&&n.push(h?e.slice(1):e);return Object.getOwnPropertySymbols?n.concat(Object.getOwnPropertySymbols(t)):n},s.prototype.listeners=function(t){var e=h?h+t:t,n=this._events[e];if(!n)return[];if(n.fn)return[n.fn];for(var r=0,i=n.length,o=new Array(i);r void, ...args: any[]): any; 11 | clearImmediate(id: any): void; 12 | /** 13 | * Checks whether the given value is a promise. 14 | * @private 15 | * @param {any} value - Value to be checked. 16 | * @return {boolean} 17 | */ 18 | isPromise(value: any): boolean; 19 | }; 20 | export { utils }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tasktimer", 3 | "version": "3.0.0", 4 | "description": "An accurate timer utility for running periodic tasks on the given interval ticks or dates. (Node and Browser)", 5 | "author": "Onur Yıldırım ", 6 | "license": "MIT", 7 | "main": "lib/tasktimer.min.js", 8 | "types": "lib/index.d.ts", 9 | "files": [ 10 | "lib/", 11 | "LICENSE" 12 | ], 13 | "scripts": { 14 | "audit": "npm audit --production", 15 | "size": "gzip-size ./lib/tasktimer.min.js", 16 | "clean": "rimraf ./lib", 17 | "build:dev": "webpack --mode=development --env.WEBPACK_OUT=development --progress --colors", 18 | "build:prod": "webpack --mode=production --env.WEBPACK_OUT=production --progress --colors", 19 | "build": "npm run clean && npm run build:dev && npm run build:prod", 20 | "test": "jest --coverage --verbose --no-cache --runInBand -c ./jest.json", 21 | "test:browser": "npm run clean && npm run build:dev && jest --verbose --no-cache --runInBand -c ./jest-puppeteer.json", 22 | "test:serve": "node ./test/_server", 23 | "coveralls": "cat ./test/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js -v", 24 | "docs": "npm run build:dev && docma" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/onury/tasktimer.git" 29 | }, 30 | "keywords": [ 31 | "timer", 32 | "interval", 33 | "tick", 34 | "task", 35 | "schedule", 36 | "timeout", 37 | "alarm", 38 | "clock", 39 | "time", 40 | "job", 41 | "work", 42 | "run" 43 | ], 44 | "bugs": { 45 | "url": "https://github.com/onury/tasktimer/issues" 46 | }, 47 | "homepage": "https://github.com/onury/tasktimer#readme", 48 | "devDependencies": { 49 | "@types/expect-puppeteer": "^3.3.1", 50 | "@types/jest": "^24.0.16", 51 | "@types/jest-environment-puppeteer": "^4.0.0", 52 | "@types/puppeteer": "^1.19.0", 53 | "coveralls": "^3.0.5", 54 | "docma": "^3.2.2", 55 | "express": "^4.17.1", 56 | "fork-ts-checker-webpack-plugin": "^1.4.3", 57 | "is-ci": "^2.0.0", 58 | "jest": "^24.8.0", 59 | "jest-cli": "^24.8.0", 60 | "jest-environment-node": "^24.8.0", 61 | "jest-puppeteer": "^4.3.0", 62 | "puppeteer": "^1.19.0", 63 | "rimraf": "^2.6.3", 64 | "table": "^5.4.5", 65 | "ts-jest": "^24.0.2", 66 | "ts-loader": "^6.0.4", 67 | "typescript": "^3.5.3", 68 | "uglifyjs-webpack-plugin": "^2.2.0", 69 | "webpack": "^4.39.0", 70 | "webpack-cli": "^3.3.6" 71 | }, 72 | "dependencies": { 73 | "eventemitter3": "^4.0.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ITaskOptions.ts: -------------------------------------------------------------------------------- 1 | import { TaskCallback } from '.'; 2 | 3 | /** 4 | * Interface for base task options. 5 | */ 6 | interface ITaskBaseOptions { 7 | /** 8 | * Specifies whether this task is currently enabled. This essentially gives 9 | * you a manual control over execution. The task will always bypass the 10 | * callback while this is set to `false`. 11 | * @type {boolean} 12 | */ 13 | enabled?: boolean; 14 | /** 15 | * Number of ticks to allow before running the task for the first time. 16 | * Default: `0` (ticks) 17 | * @type {number} 18 | */ 19 | tickDelay?: number; 20 | /** 21 | * Tick interval that the task should be run on. The unit is "ticks" (not 22 | * milliseconds). For instance, if the timer interval is `1000` 23 | * milliseconds, and we add a task with `5` tick intervals. The task will 24 | * run on every `5` seconds. Default: `1` (minimum tick interval) 25 | * @type {number} 26 | */ 27 | tickInterval?: number; 28 | /** 29 | * Total number of times the task should be run. `0` or `null` means 30 | * unlimited until `stopDate` is reached or the timer has stopped. If 31 | * `stopDate` is reached before `totalRuns` is fulfilled, task will still 32 | * be considered completed and will not be executed any more. Default: 33 | * `null` 34 | * @type {number} 35 | */ 36 | totalRuns?: number; 37 | /** 38 | * Indicates the initial date and time to start executing the task on given 39 | * interval. If omitted, task will be executed on defined tick interval, 40 | * right after the timer starts. 41 | * @type {number|Date} 42 | */ 43 | startDate?: number | Date; 44 | /** 45 | * Indicates the final date and time to execute the task. If `totalRuns` is 46 | * set and it's reached before this date; task will be considered completed 47 | * and will not be executed any more. If `stopDate` is omitted, task will 48 | * be executed until `totalRuns` is fulfilled or timer is stopped. 49 | * @type {number|Date} 50 | */ 51 | stopDate?: number | Date; 52 | /** 53 | * Whether to wrap callback in a `setImmediate()` call before executing. 54 | * This can be useful if the task is not doing any I/O or using any JS 55 | * timers but synchronously blocking the event loop. Default: `false` 56 | * @type {boolean} 57 | */ 58 | immediate?: boolean; 59 | /** 60 | * Specifies whether to remove the task (to free up memory) when task has 61 | * completed its executions (runs). For this to take affect, the task 62 | * should have `totalRuns` and/or `stopDate` configured. Default: `false` 63 | * @type {boolean} 64 | */ 65 | removeOnCompleted?: boolean; 66 | /** 67 | * The callback function of the task to be executed on each run. The task 68 | * itself is passed to this callback, as the first argument. If you're 69 | * defining an async task; either return a `Promise` or call `done()` 70 | * function which is passed as the second argument to the callback. 71 | * @type {TaskCallback} 72 | * 73 | * @example Using done() function 74 | * timer.add({ 75 | * callback(task, done) { 76 | * fs.readFile(filePath, () => done()); 77 | * } 78 | * }); 79 | * @example Returning a Promise() 80 | * timer.add({ 81 | * callback(task) { 82 | * return readFileAsync().then(result => { 83 | * // do some stuff... 84 | * }); 85 | * } 86 | * }); 87 | */ 88 | callback: TaskCallback; 89 | } 90 | 91 | /** 92 | * Interface for task options. 93 | * @extends ITaskBaseOptions 94 | */ 95 | interface ITaskOptions extends ITaskBaseOptions { 96 | /** 97 | * Unique ID of the task. Required if creating a `Task` instance directly. 98 | * Can be omitted if creating via `TaskTimer#add()` method. In this case, a 99 | * unique ID will be auto-generated in `task-{n}` format. 100 | * @type {string} 101 | */ 102 | id?: string; 103 | } 104 | 105 | export { 106 | ITaskBaseOptions, 107 | ITaskOptions 108 | }; 109 | -------------------------------------------------------------------------------- /src/ITaskTimerEvent.ts: -------------------------------------------------------------------------------- 1 | import { TaskTimer } from '.'; 2 | 3 | /** 4 | * Interface for time information for the latest run of the timer. 5 | */ 6 | interface ITaskTimerEvent { 7 | /** 8 | * Indicates the name of the event. 9 | * @type {TaskTimer.Event} 10 | */ 11 | name: TaskTimer.Event; 12 | /** 13 | * Indicates the source object fired this event. 14 | * @type {any} 15 | */ 16 | source: any; 17 | /** 18 | * Any object passed to the event emitter. This is generally a `Task` 19 | * instance if set. 20 | * @type {any} 21 | */ 22 | data?: any; 23 | /** 24 | * Any `Error` instance passed to the event emitter. This is generally a 25 | * task error instance if set. 26 | * @type {any} 27 | */ 28 | error?: Error; 29 | } 30 | 31 | export { ITaskTimerEvent }; 32 | -------------------------------------------------------------------------------- /src/ITaskTimerOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for `TaskTimer` options. 3 | */ 4 | interface ITaskTimerOptions { 5 | /** 6 | * Timer interval in milliseconds. Since the tasks run on ticks instead of 7 | * millisecond intervals; this value operates as the base resolution for 8 | * all tasks. If you are running heavy tasks, lower interval requires 9 | * higher CPU power. This value can be updated any time by setting the 10 | * `interval` property on the `TaskTimer` instance. Default: `1000` 11 | * (milliseconds) 12 | * @type {number} 13 | */ 14 | interval?: number; 15 | /** 16 | * Specifies whether the timer should auto-adjust the delay between ticks 17 | * if it's off due to task/CPU loads or clock-drifts. Note that precision 18 | * will be as high as possible but it still can be off by a few 19 | * milliseconds; depending on the CPU. Default: `true` 20 | * @type {Boolean} 21 | */ 22 | precision?: boolean; 23 | /** 24 | * Specifies whether to automatically stop the timer when all tasks are 25 | * completed. For this to take affect, all added tasks should have 26 | * `totalRuns` and/or `stopDate` configured. Default: `false` 27 | * @type {boolean} 28 | */ 29 | stopOnCompleted?: boolean; 30 | } 31 | 32 | export { ITaskTimerOptions }; 33 | -------------------------------------------------------------------------------- /src/ITimeInfo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores time information for a timer or task. 3 | */ 4 | interface ITimeInfo { 5 | /** 6 | * Indicates the start time of a timer or task. 7 | * @type {number} 8 | */ 9 | started: number; 10 | /** 11 | * Indicates the stop time of a timer or task. (`0` if still running.) 12 | * @type {number} 13 | */ 14 | stopped: number; 15 | /** 16 | * Indicates the the elapsed time of a timer or task, in milliseconds. 17 | * @type {number} 18 | */ 19 | elapsed: number; 20 | } 21 | 22 | export { ITimeInfo }; 23 | -------------------------------------------------------------------------------- /src/Task.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-empty */ 2 | 3 | import { ITaskBaseOptions, ITaskOptions, ITaskTimerEvent, ITimeInfo, TaskCallback, TaskTimer } from '.'; 4 | import { utils } from './utils'; 5 | 6 | /** 7 | * @private 8 | */ 9 | const DEFAULT_TASK_OPTIONS: ITaskOptions = Object.freeze({ 10 | enabled: true, 11 | tickDelay: 0, 12 | tickInterval: 1, 13 | totalRuns: null, 14 | startDate: null, 15 | stopDate: null, 16 | immediate: false, 17 | removeOnCompleted: false, 18 | callback: null 19 | }); 20 | 21 | /** 22 | * Represents the class that holds the configurations and the callback function 23 | * required to run a task. 24 | * @class 25 | */ 26 | class Task { 27 | 28 | /** 29 | * @private 30 | */ 31 | private _timer: TaskTimer; 32 | 33 | /** 34 | * @private 35 | */ 36 | private _markedCompleted: boolean; 37 | 38 | /** 39 | * @private 40 | */ 41 | private _: { 42 | currentRuns: number; 43 | timeOnFirstRun?: number; 44 | timeOnLastRun?: number; 45 | } & ITaskOptions; 46 | 47 | /** 48 | * Initializes a new instance of `Task` class. 49 | * @constructor 50 | * @param {ITaskOptions} options Task options. 51 | */ 52 | constructor(options: ITaskOptions) { 53 | this._init(options); 54 | } 55 | 56 | // --------------------------- 57 | // PUBLIC (INSTANCE) MEMBERS 58 | // --------------------------- 59 | 60 | /** 61 | * Gets the unique ID of the task. 62 | * @name Task#id 63 | * @type {string} 64 | * @readonly 65 | */ 66 | get id(): string { 67 | return this._.id; 68 | } 69 | 70 | /** 71 | * Specifies whether this task is currently enabled. This essentially gives 72 | * you a manual control over execution. The task will always bypass the 73 | * callback while this is set to `false`. 74 | * @name Task#enabled 75 | * @type {boolean} 76 | */ 77 | get enabled(): boolean { 78 | return this._.enabled; 79 | } 80 | set enabled(value: boolean) { 81 | this._.enabled = utils.getBool(value, true); 82 | } 83 | 84 | /** 85 | * Gets or sets the number of ticks to allow before running the task for 86 | * the first time. 87 | * @name Task#tickDelay 88 | * @type {number} 89 | */ 90 | get tickDelay(): number { 91 | return this._.tickDelay; 92 | } 93 | set tickDelay(value: number) { 94 | this._.tickDelay = utils.getNumber(value, 0, DEFAULT_TASK_OPTIONS.tickDelay); 95 | } 96 | 97 | /** 98 | * Gets or sets the tick interval that the task should be run on. The unit 99 | * is "ticks" (not milliseconds). For instance, if the timer interval is 100 | * `1000` milliseconds, and we add a task with `5` tick intervals. The task 101 | * will run on every `5` seconds. 102 | * @name Task#tickInterval 103 | * @type {number} 104 | */ 105 | get tickInterval(): number { 106 | return this._.tickInterval; 107 | } 108 | set tickInterval(value: number) { 109 | this._.tickInterval = utils.getNumber(value, 1, DEFAULT_TASK_OPTIONS.tickInterval); 110 | } 111 | 112 | /** 113 | * Gets or sets the total number of times the task should be run. `0` or 114 | * `null` means unlimited (until the timer has stopped). 115 | * @name Task#totalRuns 116 | * @type {number} 117 | */ 118 | get totalRuns(): number { 119 | return this._.totalRuns; 120 | } 121 | set totalRuns(value: number) { 122 | this._.totalRuns = utils.getNumber(value, 0, DEFAULT_TASK_OPTIONS.totalRuns); 123 | } 124 | 125 | /** 126 | * Specifies whether to wrap callback in a `setImmediate()` call before 127 | * executing. This can be useful if the task is not doing any I/O or using 128 | * any JS timers but synchronously blocking the event loop. 129 | * @name Task#immediate 130 | * @type {boolean} 131 | */ 132 | get immediate(): boolean { 133 | return this._.immediate; 134 | } 135 | set immediate(value: boolean) { 136 | this._.immediate = utils.getBool(value, false); 137 | } 138 | 139 | /** 140 | * Gets the number of times, this task has been run. 141 | * @name Task#currentRuns 142 | * @type {number} 143 | * @readonly 144 | */ 145 | get currentRuns(): number { 146 | return this._.currentRuns; 147 | } 148 | 149 | /** 150 | * Gets time information for the lifetime of a task. 151 | * `#time.started` indicates the first execution time of a task. 152 | * `#time.stopped` indicates the last execution time of a task. (`0` if still running.) 153 | * `#time.elapsed` indicates the total lifetime of a task. 154 | * @name Task#time 155 | * @type {ITimeInfo} 156 | * @readonly 157 | */ 158 | get time(): ITimeInfo { 159 | const started = this._.timeOnFirstRun || 0; 160 | const stopped = this._.timeOnLastRun || 0; 161 | return Object.freeze({ 162 | started, 163 | stopped, 164 | elapsed: stopped - started 165 | }); 166 | } 167 | 168 | /** 169 | * Gets the callback function to be executed on each run. 170 | * @name Task#callback 171 | * @type {TaskCallback} 172 | * @readonly 173 | */ 174 | get callback(): TaskCallback { 175 | return this._.callback; 176 | } 177 | 178 | /** 179 | * Gets or sets whether to remove the task (to free up memory) when task 180 | * has completed its executions (runs). For this to take affect, the task 181 | * should have `totalRuns` and/or `stopDate` configured. 182 | * @name Task#removeOnCompleted 183 | * @type {boolean} 184 | */ 185 | get removeOnCompleted(): boolean { 186 | return this._.removeOnCompleted; 187 | } 188 | set removeOnCompleted(value: boolean) { 189 | this._.removeOnCompleted = utils.getBool(value, false); 190 | } 191 | 192 | /** 193 | * Specifies whether the task has completed all runs (executions) or 194 | * `stopDate` is reached. Note that if both `totalRuns` and `stopDate` are 195 | * omitted, this will never return `true`; since the task has no execution 196 | * limit set. 197 | * @name Task#completed 198 | * @type {boolean} 199 | * @readonly 200 | */ 201 | get completed(): boolean { 202 | // return faster if already completed 203 | if (this._markedCompleted) return true; 204 | return Boolean((this.totalRuns && this.currentRuns >= this.totalRuns) 205 | || (this._.stopDate && Date.now() >= this._.stopDate)); 206 | } 207 | 208 | /** 209 | * Specifies whether the task can run on the current tick of the timer. 210 | * @private 211 | * @name Task#canRunOnTick 212 | * @type {boolean} 213 | * @readonly 214 | */ 215 | get canRunOnTick(): boolean { 216 | if (this._markedCompleted) return false; 217 | const tickCount = this._.startDate 218 | ? Math.ceil((Date.now() - Number(this._.startDate)) / this._timer.interval) 219 | : this._timer.tickCount; 220 | const timeToRun = !this._.startDate || Date.now() >= this._.startDate; 221 | const onInterval = tickCount > this.tickDelay && (tickCount - this.tickDelay) % this.tickInterval === 0; 222 | return Boolean(timeToRun && onInterval); 223 | } 224 | 225 | /** 226 | * Resets the current number of runs. This will keep the task running for 227 | * the same amount of `tickIntervals` initially configured. 228 | * @memberof Task 229 | * @chainable 230 | * 231 | * @param {ITaskBaseOptions} [options] If set, this will also re-configure the task. 232 | * 233 | * @returns {Task} 234 | */ 235 | reset(options?: ITaskBaseOptions): Task { 236 | this._.currentRuns = 0; 237 | if (options) { 238 | const id = (options as ITaskOptions).id; 239 | if (id && id !== this.id) throw new Error('Cannot change ID of a task.'); 240 | (options as ITaskOptions).id = this.id; 241 | this._init(options); 242 | } 243 | return this; 244 | } 245 | 246 | /** 247 | * Serialization to JSON. 248 | * 249 | * Never return string from `toJSON()`. It should return an object. 250 | * @private 251 | */ 252 | toJSON(): any { 253 | const obj = { 254 | ...this._ 255 | }; 256 | delete obj.callback; 257 | return obj; 258 | } 259 | 260 | // --------------------------- 261 | // PRIVATE (INSTANCE) MEMBERS 262 | // --------------------------- 263 | 264 | /** 265 | * Set reference to timer itself. 266 | * Only called by `TaskTimer`. 267 | * @private 268 | */ 269 | // @ts-ignore: TS6133: declared but never read. 270 | private _setTimer(timer: TaskTimer): void { 271 | this._timer = timer; 272 | } 273 | 274 | /** 275 | * @private 276 | */ 277 | private _emit(name: TaskTimer.Event, object: Task | Error): void { 278 | const event: ITaskTimerEvent = { 279 | name, 280 | source: this 281 | }; 282 | /* istanbul ignore else */ 283 | if (object instanceof Error) { 284 | event.error = object; 285 | } else { 286 | event.data = object; 287 | } 288 | this._timer.emit(name, event); 289 | } 290 | 291 | /** 292 | * `TaskTimer` should be informed if this task is completed. But execution 293 | * should be finished. So we do this within the `done()` function. 294 | * @private 295 | */ 296 | private _done(): void { 297 | if (this.completed) { 298 | this._markedCompleted = true; 299 | this._.timeOnLastRun = Date.now(); 300 | (this._timer as any)._taskCompleted(this); 301 | } 302 | } 303 | 304 | /** 305 | * @private 306 | */ 307 | private _execCallback(): void { 308 | try { 309 | const o = this.callback.apply(this, [this, () => this._done()]); 310 | if (this.callback.length >= 2) { 311 | // handled by done() (called within the task callback by the user) 312 | } else if (utils.isPromise(o)) { 313 | o.then(() => { 314 | this._done(); 315 | }) 316 | .catch((err: Error) => { 317 | this._emit(TaskTimer.Event.TASK_ERROR, err); 318 | }); 319 | } else { 320 | this._done(); 321 | } 322 | } catch (err) { 323 | this._emit(TaskTimer.Event.TASK_ERROR, err); 324 | } 325 | } 326 | 327 | /** 328 | * Only used by `TaskTimer`. 329 | * @private 330 | */ 331 | // @ts-ignore: TS6133: declared but never read. 332 | private _run(onRun: Function | any): void { 333 | if (!this.enabled || this._markedCompleted) return; 334 | if (this.currentRuns === 0) this._.timeOnFirstRun = Date.now(); 335 | // current runs should be set before execution or it might flow if some 336 | // async runs finishes faster and some other slower. 337 | this._.currentRuns++; 338 | onRun(); 339 | 340 | if (this.immediate) { 341 | utils.setImmediate(() => this._execCallback()); 342 | } else { 343 | this._execCallback(); 344 | } 345 | } 346 | 347 | /** 348 | * @private 349 | */ 350 | private _init(options: ITaskOptions): void { 351 | if (!options || !options.id) { 352 | throw new Error('A unique task ID is required.'); 353 | } 354 | 355 | if (typeof options.callback !== 'function') { 356 | throw new Error('A callback function is required for a task to run.'); 357 | } 358 | 359 | const { startDate, stopDate } = options; 360 | if (startDate && stopDate && startDate >= stopDate) { 361 | throw new Error('Task start date cannot be the same or after stop date.'); 362 | } 363 | 364 | this._markedCompleted = false; 365 | 366 | this._ = { 367 | currentRuns: 0, 368 | ...DEFAULT_TASK_OPTIONS 369 | }; 370 | 371 | this._.id = String(options.id); 372 | this._.callback = options.callback; 373 | this._.startDate = options.startDate || null; 374 | this._.stopDate = options.stopDate || null; 375 | 376 | // using setters for validation & default values 377 | this.enabled = options.enabled; 378 | this.tickDelay = options.tickDelay; 379 | this.tickInterval = options.tickInterval; 380 | this.totalRuns = options.totalRuns; 381 | this.immediate = options.immediate; 382 | this.removeOnCompleted = options.removeOnCompleted; 383 | } 384 | } 385 | 386 | // --------------------------- 387 | // EXPORT 388 | // --------------------------- 389 | 390 | export { Task }; 391 | -------------------------------------------------------------------------------- /src/TaskCallback.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '.'; 2 | 3 | /** 4 | * Defines a callback function for a task to be executed on each run. The task 5 | * itself is passed to this callback, as the first argument. If you're 6 | * defining an async task; either return a `Promise` or call `done()` 7 | * function which is passed as the second argument to the callback. 8 | */ 9 | export type TaskCallback = (task: Task, done?: Function) => void; 10 | -------------------------------------------------------------------------------- /src/TaskTimer.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:max-file-line-count */ 2 | 3 | // dep modules 4 | import { EventEmitter } from 'eventemitter3'; 5 | 6 | // own modules 7 | import { 8 | ITaskOptions, ITaskTimerEvent, ITaskTimerOptions, ITimeInfo, Task as TTask, TaskCallback 9 | } from '.'; 10 | import { utils } from './utils'; 11 | 12 | /** 13 | * @private 14 | */ 15 | const DEFAULT_TIMER_OPTIONS: ITaskTimerOptions = Object.freeze({ 16 | interval: 1000, 17 | precision: true, 18 | stopOnCompleted: false 19 | }); 20 | 21 | /** 22 | * TaskTimer • https://github.com/onury/tasktimer 23 | * @license MIT 24 | * @copyright 2019, Onur Yıldırım 25 | */ 26 | 27 | // --------------------------- 28 | // EventEmitter Docs 29 | // --------------------------- 30 | 31 | /** 32 | * Calls each of the listeners registered for a given event name. 33 | * @name TaskTimer#emit 34 | * @function 35 | * 36 | * @param {TaskTimer.Event} eventName - The name of the event to be emitted. 37 | * @param {any} [data] - Data to be passed to event listeners. 38 | * 39 | * @returns {Boolean} - `true` if the event had listeners, else `false`. 40 | */ 41 | 42 | /** 43 | * Return an array listing the events for which the emitter has registered 44 | * listeners. 45 | * @name TaskTimer#eventNames 46 | * @function 47 | * 48 | * @returns {Array} - List of event names. 49 | */ 50 | 51 | /** 52 | * Adds the listener function to the end of the listeners array for the event 53 | * named `eventName`. No checks are made to see if the listener has already 54 | * been added. Multiple calls passing the same combination of `eventName` and 55 | * `listener` will result in the listener being added, and called, multiple 56 | * times. 57 | * @name TaskTimer#on 58 | * @function 59 | * @alias TaskTimer#addListener 60 | * @chainable 61 | * 62 | * @param {TaskTimer.Event} eventName - The name of the event to be added. 63 | * @param {Function} listener - The callback function to be invoked per event. 64 | * @param {*} [context=this] - The context to invoke the listener with. 65 | * 66 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 67 | * 68 | * @example 69 | * const timer = new TaskTimer(1000); 70 | * // add a listener to be invoked when timer has stopped. 71 | * timer.on(TaskTimer.Event.STOPPED, () => { 72 | * console.log('Timer has stopped!'); 73 | * }); 74 | * timer.start(); 75 | */ 76 | 77 | /** 78 | * Adds a one time listener function for the event named `eventName`. The next 79 | * time `eventName` is triggered, this `listener` is removed and then invoked. 80 | * @name TaskTimer#once 81 | * @function 82 | * @chainable 83 | * 84 | * @param {TaskTimer.Event} eventName - The name of the event to be added. 85 | * @param {Function} listener - The callback function to be invoked per event. 86 | * @param {*} [context=this] - The context to invoke the listener with. 87 | * 88 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 89 | */ 90 | 91 | /** 92 | * Removes the specified `listener` from the listener array for the event 93 | * named `eventName`. 94 | * @name TaskTimer#off 95 | * @function 96 | * @alias TaskTimer#removeListener 97 | * @chainable 98 | * 99 | * @param {TaskTimer.Event} eventName - The name of the event to be removed. 100 | * @param {Function} listener - The callback function to be invoked per event. 101 | * @param {*} [context=this] - Only remove the listeners that have this context. 102 | * @param {Boolean} [once=false] - Only remove one-time listeners. 103 | * 104 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 105 | */ 106 | 107 | /** 108 | * Gets the number of listeners listening to a given event. 109 | * @name TaskTimer#listenerCount 110 | * @function 111 | * 112 | * @param {TaskTimer.Event} eventName - The name of the event. 113 | * 114 | * @returns {Number} - The number of listeners. 115 | */ 116 | 117 | /** 118 | * Gets the listeners registered for a given event. 119 | * @name TaskTimer#listeners 120 | * @function 121 | * 122 | * @param {TaskTimer.Event} eventName - The name of the event. 123 | * 124 | * @returns {Array} - The registered listeners. 125 | */ 126 | 127 | /** 128 | * Removes all listeners, or those of the specified `eventName`. 129 | * @name TaskTimer#removeAllListeners 130 | * @function 131 | * @chainable 132 | * 133 | * @param {TaskTimer.Event} [eventName] - The name of the event to be removed. 134 | * 135 | * @returns {TaskTimer} - `{@link #TaskTimer|TaskTimer}` instance. 136 | */ 137 | 138 | /** 139 | * A timer utility for running periodic tasks on the given interval ticks. This 140 | * is useful when you want to run or schedule multiple tasks on a single timer 141 | * instance. 142 | * 143 | * This class extends `EventEmitter3` which is an `EventEmitter` implementation 144 | * for both Node and browser. For detailed information, refer to Node.js 145 | * documentation. 146 | * @class 147 | * @global 148 | * 149 | * @extends EventEmitter 150 | * 151 | * @see 152 | * {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter} 153 | */ 154 | class TaskTimer extends EventEmitter { 155 | 156 | /** 157 | * Inner storage for Tasktimer. 158 | * @private 159 | */ 160 | private _: { 161 | opts: ITaskTimerOptions; 162 | state: TaskTimer.State; 163 | tasks: { [k: string]: TTask }; 164 | tickCount: number; 165 | taskRunCount: number; 166 | startTime: number; 167 | stopTime: number; 168 | completedTaskCount: number; 169 | // below are needed for precise interval. we need to inspect ticks and 170 | // elapsed time difference within the latest "continuous" session. in 171 | // other words, paused time should be ignored in these calculations. so 172 | // we need varibales saved after timer is resumed. 173 | resumeTime: number; 174 | hrResumeTime: [number, number]; 175 | tickCountAfterResume: number; 176 | }; 177 | 178 | /** 179 | * setTimeout reference used by the timmer. 180 | * @private 181 | */ 182 | private _timeoutRef: any; 183 | 184 | /** 185 | * setImmediate reference used by the timer. 186 | * @private 187 | */ 188 | private _immediateRef: any; 189 | 190 | /** 191 | * Timer run count storage. 192 | * @private 193 | */ 194 | private _runCount: number; 195 | 196 | // --------------------------- 197 | // CONSTRUCTOR 198 | // --------------------------- 199 | 200 | /** 201 | * Constructs a new `TaskTimer` instance with the given time interval (in 202 | * milliseconds). 203 | * @constructor 204 | * 205 | * @param {ITaskTimerOptions|number} [options] - Either TaskTimer options 206 | * or a base interval (in milliseconds). Since the tasks run on ticks 207 | * instead of millisecond intervals; this value operates as the base 208 | * resolution for all tasks. If you are running heavy tasks, lower interval 209 | * requires higher CPU power. This value can be updated any time by setting 210 | * the `interval` property on the instance. 211 | * 212 | * @example 213 | * const timer = new TaskTimer(1000); // milliseconds 214 | * // Execute some code on each tick... 215 | * timer.on('tick', () => { 216 | * console.log('tick count: ' + timer.tickCount); 217 | * console.log('elapsed time: ' + timer.time.elapsed + ' ms.'); 218 | * }); 219 | * // add a task named 'heartbeat' that runs every 5 ticks and a total of 10 times. 220 | * const task1 = { 221 | * id: 'heartbeat', 222 | * tickDelay: 20, // ticks (to wait before first run) 223 | * tickInterval: 5, // ticks (interval) 224 | * totalRuns: 10, // times to run 225 | * callback(task) { // can also be an async function, returning a promise 226 | * console.log(task.id + ' task has run ' + task.currentRuns + ' times.'); 227 | * } 228 | * }; 229 | * timer.add(task1).start(); 230 | */ 231 | constructor(options?: ITaskTimerOptions | number) { 232 | super(); 233 | 234 | this._timeoutRef = null; 235 | this._immediateRef = null; 236 | this._runCount = 0; 237 | this._reset(); 238 | 239 | this._.opts = {}; 240 | const opts = typeof options === 'number' 241 | ? { interval: options } 242 | : options || {} as any; 243 | this.interval = opts.interval; 244 | this.precision = opts.precision; 245 | this.stopOnCompleted = opts.stopOnCompleted; 246 | } 247 | 248 | // --------------------------- 249 | // PUBLIC (INSTANCE) PROPERTIES 250 | // --------------------------- 251 | 252 | /** 253 | * Gets or sets the base timer interval in milliseconds. 254 | * 255 | * Since the tasks run on ticks instead of millisecond intervals; this 256 | * value operates as the base resolution for all tasks. If you are running 257 | * heavy tasks, lower interval requires higher CPU power. This value can be 258 | * updated any time. 259 | * 260 | * @name TaskTimer#interval 261 | * @type {number} 262 | */ 263 | get interval(): number { 264 | return this._.opts.interval; 265 | } 266 | set interval(value: number) { 267 | this._.opts.interval = utils.getNumber(value, 20, DEFAULT_TIMER_OPTIONS.interval); 268 | } 269 | 270 | /** 271 | * Gets or sets whether timer precision enabled. 272 | * 273 | * Because of the single-threaded, asynchronous nature of JavaScript, each 274 | * execution takes a piece of CPU time, and the time they have to wait will 275 | * vary, depending on the load. This creates a latency and cumulative 276 | * difference in asynchronous timers; that gradually increase the 277 | * inacuraccy. `TaskTimer` overcomes this problem as much as possible: 278 | * 279 | *
  • The delay between each tick is auto-adjusted when it's off 280 | * due to task/CPU loads or clock drifts.
  • 281 | *
  • In Node.js, `TaskTimer` also makes use of `process.hrtime()` 282 | * high-resolution real-time. The time is relative to an arbitrary 283 | * time in the past (not related to the time of day) and therefore not 284 | * subject to clock drifts.
  • 285 | *
  • The timer may hit a synchronous / blocking task; or detect significant 286 | * time drift (longer than the base interval) due to JS event queue, which 287 | * cannot be recovered by simply adjusting the next delay. In this case, right 288 | * from the next tick onward; it will auto-recover as much as possible by 289 | * running "immediate" tasks until it reaches the proper time vs tick/run 290 | * balance.
  • 291 | * 292 | *
    Note that precision will be as high as possible but it still 293 | * can be off by a few milliseconds; depending on the CPU or the load. 294 | *
    295 | * @name TaskTimer#precision 296 | * @type {boolean} 297 | */ 298 | get precision(): boolean { 299 | return this._.opts.precision; 300 | } 301 | set precision(value: boolean) { 302 | this._.opts.precision = utils.getBool(value, DEFAULT_TIMER_OPTIONS.precision); 303 | } 304 | 305 | /** 306 | * Gets or sets whether the timer should automatically stop when all tasks 307 | * are completed. For this to take affect, all added tasks should have 308 | * `totalRuns` and/or `stopDate` configured. This option can be set/changed 309 | * at any time. 310 | * @name TaskTimer#stopOnCompleted 311 | * @type {boolean} 312 | */ 313 | get stopOnCompleted(): boolean { 314 | return this._.opts.stopOnCompleted; 315 | } 316 | set stopOnCompleted(value: boolean) { 317 | this._.opts.stopOnCompleted = utils.getBool(value, DEFAULT_TIMER_OPTIONS.stopOnCompleted); 318 | } 319 | 320 | /** 321 | * Gets the current state of the timer. 322 | * For possible values, see `TaskTimer.State` enumeration. 323 | * @name TaskTimer#state 324 | * @type {TaskTimer.State} 325 | * @readonly 326 | */ 327 | get state(): TaskTimer.State { 328 | return this._.state; 329 | } 330 | 331 | /** 332 | * Gets time information for the latest run of the timer. 333 | * `#time.started` indicates the start time of the timer. 334 | * `#time.stopped` indicates the stop time of the timer. (`0` if still running.) 335 | * `#time.elapsed` indicates the elapsed time of the timer. 336 | * @name TaskTimer#time 337 | * @type {ITimeInfo} 338 | * @readonly 339 | */ 340 | get time(): ITimeInfo { 341 | const { startTime, stopTime } = this._; 342 | const t: ITimeInfo = { 343 | started: startTime, 344 | stopped: stopTime, 345 | elapsed: 0 346 | }; 347 | if (startTime) { 348 | const current = this.state !== TaskTimer.State.STOPPED ? Date.now() : stopTime; 349 | t.elapsed = current - startTime; 350 | } 351 | return Object.freeze(t); 352 | } 353 | 354 | /** 355 | * Gets the current tick count for the latest run of the timer. 356 | * This value will be reset to `0` when the timer is stopped or reset. 357 | * @name TaskTimer#tickCount 358 | * @type {Number} 359 | * @readonly 360 | */ 361 | get tickCount(): number { 362 | return this._.tickCount; 363 | } 364 | 365 | /** 366 | * Gets the current task count. Tasks remain even after the timer is 367 | * stopped. But they will be removed if the timer is reset. 368 | * @name TaskTimer#taskCount 369 | * @type {Number} 370 | * @readonly 371 | */ 372 | get taskCount(): number { 373 | return Object.keys(this._.tasks).length; 374 | } 375 | 376 | /** 377 | * Gets the total number of all task executions (runs). 378 | * @name TaskTimer#taskRunCount 379 | * @type {Number} 380 | * @readonly 381 | */ 382 | get taskRunCount(): number { 383 | return this._.taskRunCount; 384 | } 385 | 386 | /** 387 | * Gets the total number of timer runs, including resumed runs. 388 | * @name TaskTimer#runCount 389 | * @type {Number} 390 | * @readonly 391 | */ 392 | get runCount(): number { 393 | return this._runCount; 394 | } 395 | 396 | // --------------------------- 397 | // PUBLIC (INSTANCE) METHODS 398 | // --------------------------- 399 | 400 | /** 401 | * Gets the task with the given ID. 402 | * @memberof TaskTimer 403 | * 404 | * @param {String} id - ID of the task. 405 | * 406 | * @returns {Task} 407 | */ 408 | get(id: string): TTask { 409 | return this._.tasks[id] || null; 410 | } 411 | 412 | /** 413 | * Adds a collection of new tasks for the timer. 414 | * @memberof TaskTimer 415 | * @chainable 416 | * 417 | * @param {Task|ITaskOptions|TaskCallback|Array} task - Either a 418 | * single task, task options object or the callback function; or a mixture 419 | * of these as an array. 420 | * 421 | * @returns {TaskTimer} 422 | * 423 | * @throws {Error} - If a task callback is not set or a task with the given 424 | * name already exists. 425 | */ 426 | add(task: TTask | ITaskOptions | TaskCallback | Array): TaskTimer { 427 | if (!utils.isset(task)) { 428 | throw new Error('Either a task, task options or a callback is required.'); 429 | } 430 | utils.ensureArray(task).forEach((item: any) => this._add(item)); 431 | return this; 432 | } 433 | 434 | /** 435 | * Removes the task by the given name. 436 | * @memberof TaskTimer 437 | * @chainable 438 | * 439 | * @param {string|Task} task - Task to be removed. Either pass the 440 | * name or the task itself. 441 | * 442 | * @returns {TaskTimer} 443 | * 444 | * @throws {Error} - If a task with the given name does not exist. 445 | */ 446 | remove(task: string | TTask): TaskTimer { 447 | const id: string = typeof task === 'string' ? task : task.id; 448 | task = this.get(id); 449 | 450 | if (!id || !task) { 451 | throw new Error(`No tasks exist with ID: '${id}'.`); 452 | } 453 | 454 | // first decrement completed tasks count if this is a completed task. 455 | if (task.completed && this._.completedTaskCount > 0) this._.completedTaskCount--; 456 | 457 | this._.tasks[id] = null; 458 | delete this._.tasks[id]; 459 | this._emit(TaskTimer.Event.TASK_REMOVED, task); 460 | return this; 461 | } 462 | 463 | /** 464 | * Starts the timer and puts the timer in `RUNNING` state. If it's already 465 | * running, this will reset the start/stop time and tick count, but will not 466 | * reset (or remove) existing tasks. 467 | * @memberof TaskTimer 468 | * @chainable 469 | * 470 | * @returns {TaskTimer} 471 | */ 472 | start(): TaskTimer { 473 | this._stop(); 474 | this._.state = TaskTimer.State.RUNNING; 475 | this._runCount++; 476 | this._.tickCount = 0; 477 | this._.taskRunCount = 0; 478 | this._.stopTime = 0; 479 | this._markTime(); 480 | this._.startTime = Date.now(); 481 | this._emit(TaskTimer.Event.STARTED); 482 | this._run(); 483 | return this; 484 | } 485 | 486 | /** 487 | * Pauses the timer, puts the timer in `PAUSED` state and all tasks on hold. 488 | * @memberof TaskTimer 489 | * @chainable 490 | * 491 | * @returns {TaskTimer} 492 | */ 493 | pause(): TaskTimer { 494 | if (this.state !== TaskTimer.State.RUNNING) return this; 495 | this._stop(); 496 | this._.state = TaskTimer.State.PAUSED; 497 | this._emit(TaskTimer.Event.PAUSED); 498 | return this; 499 | } 500 | 501 | /** 502 | * Resumes the timer and puts the timer in `RUNNING` state; if previuosly 503 | * paused. In this state, all existing tasks are resumed. 504 | * @memberof TaskTimer 505 | * @chainable 506 | * 507 | * @returns {TaskTimer} 508 | */ 509 | resume(): TaskTimer { 510 | if (this.state === TaskTimer.State.IDLE) { 511 | this.start(); 512 | return this; 513 | } 514 | if (this.state !== TaskTimer.State.PAUSED) return this; 515 | this._runCount++; 516 | this._markTime(); 517 | this._.state = TaskTimer.State.RUNNING; 518 | this._emit(TaskTimer.Event.RESUMED); 519 | this._run(); 520 | return this; 521 | } 522 | 523 | /** 524 | * Stops the timer and puts the timer in `STOPPED` state. In this state, all 525 | * existing tasks are stopped and no values or tasks are reset until 526 | * re-started or explicitly calling reset. 527 | * @memberof TaskTimer 528 | * @chainable 529 | * 530 | * @returns {TaskTimer} 531 | */ 532 | stop(): TaskTimer { 533 | if (this.state !== TaskTimer.State.RUNNING) return this; 534 | this._stop(); 535 | this._.stopTime = Date.now(); 536 | this._.state = TaskTimer.State.STOPPED; 537 | this._emit(TaskTimer.Event.STOPPED); 538 | return this; 539 | } 540 | 541 | /** 542 | * Stops the timer and puts the timer in `IDLE` state. 543 | * This will reset the ticks and removes all tasks silently; meaning no 544 | * other events will be emitted such as `"taskRemoved"`. 545 | * @memberof TaskTimer 546 | * @chainable 547 | * 548 | * @returns {TaskTimer} 549 | */ 550 | reset(): TaskTimer { 551 | this._reset(); 552 | this._emit(TaskTimer.Event.RESET); 553 | return this; 554 | } 555 | 556 | // --------------------------- 557 | // PRIVATE (INSTANCE) METHODS 558 | // --------------------------- 559 | 560 | /** 561 | * @private 562 | */ 563 | private _emit(type: TaskTimer.Event, data?: any): boolean { 564 | const event: ITaskTimerEvent = { 565 | name: type, 566 | source: this, 567 | data 568 | }; 569 | return this.emit(type, event); 570 | } 571 | 572 | /** 573 | * Adds a new task for the timer. 574 | * @private 575 | * 576 | * @param {Task|ITaskOptions|TaskCallback} options - Either a task instance, 577 | * task options object or the callback function to be executed on tick 578 | * intervals. 579 | * 580 | * @returns {TaskTimer} 581 | * 582 | * @throws {Error} - If the task callback is not set or a task with the 583 | * given name already exists. 584 | */ 585 | private _add(options: TTask | ITaskOptions | TaskCallback): TaskTimer { 586 | if (typeof options === 'function') { 587 | options = { 588 | callback: options 589 | }; 590 | } 591 | 592 | if (utils.type(options) === 'object' && !options.id) { 593 | (options as ITaskOptions).id = this._getUniqueTaskID(); 594 | } 595 | 596 | if (this.get(options.id)) { 597 | throw new Error(`A task with id '${options.id}' already exists.`); 598 | } 599 | 600 | const task = options instanceof TTask ? options : new TTask(options); 601 | (task as any)._setTimer(this); 602 | this._.tasks[task.id] = task; 603 | this._emit(TaskTimer.Event.TASK_ADDED, task); 604 | return this; 605 | } 606 | 607 | /** 608 | * Stops the timer. 609 | * @private 610 | */ 611 | private _stop(): void { 612 | this._.tickCountAfterResume = 0; 613 | if (this._timeoutRef) { 614 | clearTimeout(this._timeoutRef); 615 | this._timeoutRef = null; 616 | } 617 | if (this._immediateRef) { 618 | utils.clearImmediate(this._immediateRef); 619 | this._immediateRef = null; 620 | } 621 | } 622 | 623 | /** 624 | * Resets the timer. 625 | * @private 626 | */ 627 | private _reset(): void { 628 | this._ = { 629 | opts: (this._ || {} as any).opts, 630 | state: TaskTimer.State.IDLE, 631 | tasks: {}, 632 | tickCount: 0, 633 | taskRunCount: 0, 634 | startTime: 0, 635 | stopTime: 0, 636 | completedTaskCount: 0, 637 | resumeTime: 0, 638 | hrResumeTime: null, 639 | tickCountAfterResume: 0 640 | }; 641 | this._stop(); 642 | } 643 | 644 | /** 645 | * Called (by Task instance) when it has completed all of its runs. 646 | * @private 647 | */ 648 | // @ts-ignore: TS6133: declared but never read. 649 | private _taskCompleted(task: TTask): void { 650 | this._.completedTaskCount++; 651 | this._emit(TaskTimer.Event.TASK_COMPLETED, task); 652 | if (this._.completedTaskCount === this.taskCount) { 653 | this._emit(TaskTimer.Event.COMPLETED); 654 | if (this.stopOnCompleted) this.stop(); 655 | } 656 | if (task.removeOnCompleted) this.remove(task); 657 | } 658 | 659 | /** 660 | * Handler to be executed on each tick. 661 | * @private 662 | */ 663 | private _tick(): void { 664 | this._.state = TaskTimer.State.RUNNING; 665 | 666 | let id: string; 667 | let task: TTask; 668 | const tasks = this._.tasks; 669 | 670 | this._.tickCount++; 671 | this._.tickCountAfterResume++; 672 | this._emit(TaskTimer.Event.TICK); 673 | 674 | // tslint:disable:forin 675 | for (id in tasks) { 676 | task = tasks[id]; 677 | if (!task || !task.canRunOnTick) continue; 678 | 679 | // below will not execute if task is disabled or already 680 | // completed. 681 | (task as any)._run(() => { 682 | this._.taskRunCount++; 683 | this._emit(TaskTimer.Event.TASK, task); 684 | }); 685 | } 686 | 687 | this._run(); 688 | } 689 | 690 | /** 691 | * Marks the resume (or start) time in milliseconds or high-resolution time 692 | * if available. 693 | * @private 694 | */ 695 | private _markTime(): void { 696 | /* istanbul ignore if */ 697 | if (utils.BROWSER) { // tested separately 698 | this._.resumeTime = Date.now(); 699 | } else { 700 | this._.hrResumeTime = process.hrtime(); 701 | } 702 | } 703 | 704 | /** 705 | * Gets the time difference in milliseconds sinct the last resume or start 706 | * time. 707 | * @private 708 | */ 709 | private _getTimeDiff(): number { 710 | // Date.now() is ~2x faster than Date#getTime() 711 | /* istanbul ignore if */ 712 | if (utils.BROWSER) return Date.now() - this._.resumeTime; // tested separately 713 | 714 | const hrDiff = process.hrtime(this._.hrResumeTime); 715 | return Math.ceil((hrDiff[0] * 1000) + (hrDiff[1] / 1e6)); 716 | } 717 | 718 | /** 719 | * Runs the timer. 720 | * @private 721 | */ 722 | private _run(): void { 723 | if (this.state !== TaskTimer.State.RUNNING) return; 724 | 725 | let interval = this.interval; 726 | // we'll get a precise interval by checking if our clock is already 727 | // drifted. 728 | if (this.precision) { 729 | const diff = this._getTimeDiff(); 730 | // did we reach this expected tick count for the given time period? 731 | // calculated count should not be greater than tickCountAfterResume 732 | if (Math.floor(diff / interval) > this._.tickCountAfterResume) { 733 | // if we're really late, run immediately! 734 | this._immediateRef = utils.setImmediate(() => this._tick()); 735 | return; 736 | } 737 | // if we still have time but a bit off, update next interval. 738 | interval = interval - (diff % interval); 739 | } 740 | 741 | this._timeoutRef = setTimeout(() => this._tick(), interval); 742 | } 743 | 744 | /** 745 | * Gets a unique task ID. 746 | * @private 747 | */ 748 | private _getUniqueTaskID(): string { 749 | let num: number = this.taskCount; 750 | let id: string; 751 | while (!id || this.get(id)) { 752 | num++; 753 | id = 'task' + num; 754 | } 755 | return id; 756 | } 757 | } 758 | 759 | // --------------------------- 760 | // NAMESPACE 761 | // --------------------------- 762 | 763 | // tslint:disable:no-namespace 764 | /* istanbul ignore next */ 765 | /** @private */ 766 | namespace TaskTimer { 767 | 768 | /** 769 | * Represents the class that holds the configurations and the callback function 770 | * required to run a task. See {@link api/#Task|class information}. 771 | * @name TaskTimer.Task 772 | * @class 773 | */ 774 | export const Task = TTask; 775 | 776 | /** 777 | * Enumerates `TaskTimer` states. 778 | * @memberof TaskTimer 779 | * @enum {String} 780 | * @readonly 781 | */ 782 | export enum State { 783 | /** 784 | * Indicates that the timer is in `idle` state. 785 | * This is the initial state when the `TaskTimer` instance is first created. 786 | * Also when an existing timer is reset, it will be `idle`. 787 | * @type {String} 788 | */ 789 | IDLE = 'idle', 790 | /** 791 | * Indicates that the timer is in `running` state; such as when the timer is 792 | * started or resumed. 793 | * @type {String} 794 | */ 795 | RUNNING = 'running', 796 | /** 797 | * Indicates that the timer is in `paused` state. 798 | * @type {String} 799 | */ 800 | PAUSED = 'paused', 801 | /** 802 | * Indicates that the timer is in `stopped` state. 803 | * @type {String} 804 | */ 805 | STOPPED = 'stopped' 806 | } 807 | 808 | /** 809 | * Enumerates the `TaskTimer` event types. 810 | * @memberof TaskTimer 811 | * @enum {String} 812 | * @readonly 813 | */ 814 | export enum Event { 815 | /** 816 | * Emitted on each tick (interval) of `TaskTimer`. 817 | * @type {String} 818 | */ 819 | TICK = 'tick', 820 | /** 821 | * Emitted when the timer is put in `RUNNING` state; such as when the timer is 822 | * started. 823 | * @type {String} 824 | */ 825 | STARTED = 'started', 826 | /** 827 | * Emitted when the timer is put in `RUNNING` state; such as when the timer is 828 | * resumed. 829 | * @type {String} 830 | */ 831 | RESUMED = 'resumed', 832 | /** 833 | * Emitted when the timer is put in `PAUSED` state. 834 | * @type {String} 835 | */ 836 | PAUSED = 'paused', 837 | /** 838 | * Emitted when the timer is put in `STOPPED` state. 839 | * @type {String} 840 | */ 841 | STOPPED = 'stopped', 842 | /** 843 | * Emitted when the timer is reset. 844 | * @type {String} 845 | */ 846 | RESET = 'reset', 847 | /** 848 | * Emitted when a task is executed. 849 | * @type {String} 850 | */ 851 | TASK = 'task', 852 | /** 853 | * Emitted when a task is added to `TaskTimer` instance. 854 | * @type {String} 855 | */ 856 | TASK_ADDED = 'taskAdded', 857 | /** 858 | * Emitted when a task is removed from `TaskTimer` instance. 859 | * Note that this will not be emitted when `.reset()` is called; which 860 | * removes all tasks silently. 861 | * @type {String} 862 | */ 863 | TASK_REMOVED = 'taskRemoved', 864 | /** 865 | * Emitted when a task has completed all of its executions (runs) 866 | * or reached its stopping date/time (if set). Note that this event 867 | * will only be fired if the tasks has a `totalRuns` limit or a 868 | * `stopDate` value set. 869 | * @type {String} 870 | */ 871 | TASK_COMPLETED = 'taskCompleted', 872 | /** 873 | * Emitted when a task produces an error on its execution. 874 | * @type {String} 875 | */ 876 | TASK_ERROR = 'taskError', 877 | /** 878 | * Emitted when all tasks have completed all of their executions (runs) 879 | * or reached their stopping date/time (if set). Note that this event 880 | * will only be fired if all tasks have a `totalRuns` limit or a 881 | * `stopDate` value set. 882 | * @type {String} 883 | */ 884 | COMPLETED = 'completed' 885 | } 886 | } 887 | 888 | // --------------------------- 889 | // EXPORT 890 | // --------------------------- 891 | 892 | export { TaskTimer }; 893 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ITaskOptions'; 2 | export * from './ITaskTimerOptions'; 3 | export * from './ITaskTimerEvent'; 4 | export * from './ITimeInfo'; 5 | export * from './Task'; 6 | export * from './TaskCallback'; 7 | export * from './TaskTimer'; 8 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | const proto = Object.prototype; 3 | const NODE = typeof setImmediate === 'function' 4 | && typeof process === 'object' 5 | && typeof process.hrtime === 'function'; 6 | const BROWSER = !NODE; 7 | 8 | /** @private */ 9 | const utils = { 10 | NODE, 11 | BROWSER, 12 | type(o: any): string { 13 | return proto.toString.call(o).match(/\s(\w+)/i)[1].toLowerCase(); 14 | }, 15 | isset(o: any): boolean { 16 | return o !== null && o !== undefined; 17 | }, 18 | ensureArray(o: any): any[] { 19 | return utils.isset(o) 20 | ? !Array.isArray(o) ? [o] : o 21 | : []; 22 | }, 23 | getNumber(value: number, minimum: number, defaultValue: number): number { 24 | return typeof value === 'number' 25 | ? (value < minimum ? minimum : value) 26 | : defaultValue; 27 | }, 28 | getBool(value: boolean, defaultValue: boolean): boolean { 29 | return typeof value !== 'boolean' 30 | ? defaultValue 31 | : value; 32 | }, 33 | setImmediate(cb: (...args: any[]) => void, ...args: any[]): any { 34 | /* istanbul ignore if */ 35 | if (utils.BROWSER) { // tested separately 36 | return setTimeout(cb.apply(null, args), 0); 37 | } 38 | return setImmediate(cb, ...args); 39 | }, 40 | clearImmediate(id: any): void { 41 | /* istanbul ignore next */ 42 | if (!id) return; 43 | /* istanbul ignore if */ 44 | if (utils.BROWSER) return clearTimeout(id); // tested separately 45 | clearImmediate(id); 46 | }, 47 | /** 48 | * Checks whether the given value is a promise. 49 | * @private 50 | * @param {any} value - Value to be checked. 51 | * @return {boolean} 52 | */ 53 | isPromise(value: any): boolean { 54 | return value 55 | && utils.type(value) === 'promise' 56 | && typeof value.then === 'function'; 57 | } 58 | }; 59 | 60 | export { utils }; 61 | -------------------------------------------------------------------------------- /tasktimer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onury/tasktimer/aff7e351c2f79b9764ffab651f4331b9c47e7c2c/tasktimer-logo.png -------------------------------------------------------------------------------- /test/_server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const express = require('express'); 5 | 6 | const app = express(); 7 | app.use(express.static(path.join(__dirname, 'public'))); 8 | app.use(express.static(path.join(__dirname, '../../lib'))); 9 | app.listen(5001); -------------------------------------------------------------------------------- /test/_server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TaskTimer 8 | 9 | 10 | 11 | 12 | 13 |
    14 |

    15 | Jest-Puppeteer Test Page for:
    16 |

    TaskTimer

    17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /test/_server/public/manual-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TaskTimer 8 | 9 | 34 | 35 | 36 | 37 |
    38 |

    39 | Jest-Puppeteer Test Page for:
    40 |

    TaskTimer

    41 |
    42 | 43 | 44 | -------------------------------------------------------------------------------- /test/browser/tasktimer.spec.js: -------------------------------------------------------------------------------- 1 | // https://github.com/smooth-code/jest-puppeteer 2 | // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md 3 | 4 | const { TaskTimer, Task } = require('../../lib/tasktimer'); 5 | 6 | /** 7 | * TaskTimer Test Suite for Browser/Puppeteer. 8 | * This suite includes some basic tests only. 9 | * For full-coverage tests we use ./test/node/tasktimer.spec.ts 10 | */ 11 | describe('TaskTimer (Browser/Puppeteer)', () => { 12 | 13 | async function getHandle() { 14 | const handle = await page.evaluateHandle(() => window); 15 | const properties = await handle.getProperties(); 16 | return { 17 | handle, // use as `await handle.dispose()` when done (but jest-puppeteer does it for us it seems) 18 | window: properties.get('window') 19 | }; 20 | } 21 | 22 | // we use runTests() to auto-run an expect().toEqual() on each value of a 23 | // test results object. The reason is that, expect is not available within 24 | // page.evaluate() context, and writing both a test statement and an 25 | // expect() for the result value of the same statement outside 26 | // page.evaluate() is too much duplication. 27 | // ( Maybe there is a better way, dunno... ¯\_(ツ)_/¯ ) 28 | 29 | // each property of tests argument should be an `Array(2)`. 30 | // e.g. [received, expected] 31 | // correct » tests.innerText = [elem.innerText, 'hello']; 32 | // incorrect » tests.innerText = 'hello'; 33 | function runTests(tests) { 34 | let received, expected; 35 | for (let testName in tests) { 36 | received = tests[testName][0]; 37 | expected = tests[testName][1]; 38 | // if any expect fails in this iteration, the test's stack output 39 | // will not show the proper line. so we'll log if any is `false` to 40 | // get a proper hint. 41 | if (received !== expected) console.log(`FAILED: tests.${testName}`); 42 | expect(expected).toEqual(received); 43 | } 44 | } 45 | 46 | beforeAll(async () => { 47 | await page.goto('http://localhost:5001'); 48 | }); 49 | 50 | test('page / html', async () => { 51 | // "TaskTimer" text on page 52 | await expect(page).toMatch('TaskTimer'); 53 | }); 54 | 55 | test('window.tasktimer', async () => { 56 | const result = await page.evaluate(() => { 57 | const tests = {}; 58 | const { TaskTimer, Task } = window.tasktimer; 59 | tests.TaskTimer = [typeof TaskTimer.constructor, 'function']; 60 | tests.Task = [typeof Task.constructor, 'function']; 61 | return tests; 62 | }); 63 | runTests(result); 64 | }); 65 | 66 | test('TaskTimer', async () => { 67 | const result = await page.evaluate(() => { 68 | const tests = {}; 69 | const { TaskTimer } = window.tasktimer; 70 | 71 | const timer = new TaskTimer(); 72 | tests.interval = [timer.interval, 1000]; 73 | tests.stopOnCompleted = [timer.stopOnCompleted, false]; 74 | 75 | timer.add(() => { }).start(); 76 | tests.running = [timer.state, TaskTimer.State.RUNNING]; 77 | tests.taskCount = [timer.taskCount, 1]; 78 | 79 | timer.stop(); 80 | tests.stopped = [timer.state, TaskTimer.State.STOPPED]; 81 | 82 | return tests; 83 | }); 84 | runTests(result); 85 | }); 86 | 87 | }); 88 | -------------------------------------------------------------------------------- /test/node/tasktimer.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-empty max-file-line-count */ 2 | 3 | import isCI from 'is-ci'; 4 | 5 | import { ITaskOptions, ITaskTimerEvent, Task, TaskTimer } from '../../src'; 6 | 7 | /** 8 | * TaskTimer Test Suite for Node/TypeScript. 9 | */ 10 | describe('TaskTimer (Node/TypeScript)', () => { 11 | 12 | test('TaskTimer namespace', () => { 13 | expect(TaskTimer.State).toEqual(expect.any(Object)); 14 | expect(TaskTimer.Event).toEqual(expect.any(Object)); 15 | expect(TaskTimer.Task).toEqual(Task); 16 | }); 17 | 18 | test('ITaskTimerOptions, ITaskOptions, defaults', () => { 19 | let timer = new TaskTimer(); 20 | expect(timer.interval).toEqual(1000); 21 | expect(timer.stopOnCompleted).toEqual(false); 22 | expect(timer.time).toEqual({ started: 0, stopped: 0, elapsed: 0 }); 23 | 24 | timer = new TaskTimer({ 25 | interval: 2500, 26 | stopOnCompleted: true 27 | }); 28 | expect(timer.interval).toEqual(2500); 29 | expect(timer.stopOnCompleted).toEqual(true); 30 | 31 | // name is required if constructing Task instance. 32 | expect(() => new Task({ callback: () => {} })).toThrow(); 33 | // name is NOT required if creating via TaskTimer#add() 34 | expect(() => timer.add({ callback: () => {} })).not.toThrow(); 35 | 36 | let task = new Task({ 37 | id: 'my-task', 38 | callback: () => {} 39 | }); 40 | expect(task.enabled).toEqual(true); 41 | expect(task.id).toEqual('my-task'); 42 | expect(task.tickDelay).toEqual(0); 43 | expect(task.tickInterval).toEqual(1); 44 | expect(task.totalRuns).toEqual(null); 45 | expect(task.removeOnCompleted).toEqual(false); 46 | expect(task.immediate).toEqual(false); 47 | 48 | task = timer.add({ callback: () => {} }).get('task1'); 49 | expect(task.enabled).toEqual(true); 50 | expect(task.id).toEqual('task1'); 51 | expect(task.tickDelay).toEqual(0); 52 | expect(task.tickInterval).toEqual(1); 53 | expect(task.totalRuns).toEqual(null); 54 | expect(task.removeOnCompleted).toEqual(false); 55 | expect(task.immediate).toEqual(false); 56 | 57 | const callback = o => o; 58 | task = new Task({ 59 | id: 'my-task-2', 60 | enabled: false, 61 | tickDelay: 10, 62 | tickInterval: 5, 63 | totalRuns: 3, 64 | removeOnCompleted: true, 65 | immediate: true, 66 | callback 67 | }); 68 | expect(task.enabled).toEqual(false); 69 | expect(task.id).toEqual('my-task-2'); 70 | expect(task.tickDelay).toEqual(10); 71 | expect(task.tickInterval).toEqual(5); 72 | expect(task.totalRuns).toEqual(3); 73 | expect(task.removeOnCompleted).toEqual(true); 74 | expect(task.immediate).toEqual(true); 75 | expect(task.callback).toEqual(callback); 76 | }); 77 | 78 | test('TaskTimer » .State, #start(), #pause(), #resume(), #stop(), #reset()', () => { 79 | const timer = new TaskTimer(500); 80 | expect(timer.state).toEqual(TaskTimer.State.IDLE); 81 | timer.start(); 82 | expect(timer.state).toEqual(TaskTimer.State.RUNNING); 83 | timer.pause(); 84 | expect(timer.state).toEqual(TaskTimer.State.PAUSED); 85 | timer.resume(); 86 | expect(timer.state).toEqual(TaskTimer.State.RUNNING); 87 | timer.resume(); 88 | expect(timer.state).toEqual(TaskTimer.State.RUNNING); 89 | timer.stop(); 90 | expect(timer.state).toEqual(TaskTimer.State.STOPPED); 91 | timer.reset(); 92 | expect(timer.state).toEqual(TaskTimer.State.IDLE); 93 | }); 94 | 95 | test('TaskTimer » #add(), #remove()', (done: Function) => { 96 | const interval = 500; 97 | const timer = new TaskTimer(interval); 98 | expect(timer.taskCount).toEqual(0); 99 | 100 | const task1Opts: ITaskOptions = { 101 | id: 'heartbeat', 102 | tickInterval: 2, 103 | totalRuns: 2, 104 | callback(task: Task): void { 105 | console.log(task.id + ' task has run ' + task.currentRuns + ' times @ ', timer.time.elapsed); 106 | expect(task.id).toEqual('heartbeat'); 107 | expect(timer.tickCount % 2).toEqual(0); 108 | expect(task.tickInterval).toEqual(task1Opts.tickInterval); 109 | expect(task.totalRuns).toEqual(task1Opts.totalRuns); 110 | expect(task.currentRuns <= task.totalRuns).toBeTruthy(); 111 | expect(timer.time.stopped).toEqual(0); 112 | const i = task.tickInterval * task.currentRuns; 113 | expect(timer.time.elapsed).toBeGreaterThanOrEqual(i * interval); 114 | expect(timer.time.elapsed).toBeLessThanOrEqual((i + 1) * interval); 115 | } 116 | }; 117 | timer.add(task1Opts); 118 | expect(timer.taskCount).toEqual(1); 119 | 120 | const task2: Task = new Task({ 121 | id: 'remove-check', 122 | tickInterval: 5, 123 | totalRuns: 1, 124 | removeOnCompleted: true, 125 | callback(task: Task): void { 126 | console.log(task.id + ' task has run ' + task.currentRuns + ' times.'); 127 | expect(timer.tickCount).toEqual(5); 128 | expect(timer.get('remove-check')).not.toEqual(null); 129 | } 130 | }); 131 | timer.add(task2); 132 | expect(timer.taskCount).toEqual(2); 133 | 134 | const task3: ITaskOptions = { 135 | id: 'final-check', 136 | tickInterval: 7, 137 | totalRuns: 1, 138 | callback(task: Task): void { 139 | console.log(task.id + ' task has run ' + task.currentRuns + ' times.'); 140 | expect(timer.tickCount).toEqual(7); 141 | 142 | expect(timer.get('remove-check')).toEqual(null); 143 | expect(timer.taskCount).toEqual(2); 144 | timer.remove('heartbeat'); 145 | expect(timer.taskCount).toEqual(1); 146 | 147 | timer.stop(); 148 | expect(timer.state).toEqual(TaskTimer.State.STOPPED); 149 | expect(timer.time.stopped).not.toEqual(0); 150 | timer.reset(); 151 | expect(timer.taskCount).toEqual(0); 152 | expect(timer.state).toEqual(TaskTimer.State.IDLE); 153 | 154 | done(); 155 | } 156 | }; 157 | timer.add(task3); 158 | expect(timer.taskCount).toEqual(3); 159 | 160 | // cannot add a task with existing id 161 | const t1 = { 162 | id: task1Opts.id, 163 | callback(): void {} 164 | }; 165 | expect(() => timer.add(t1)).toThrow(); 166 | // cannot add a task without a callback 167 | const t2: any = { 168 | id: 'no-callback' 169 | }; 170 | expect(() => timer.add(t2)).toThrow(); 171 | expect(() => timer.add(null)).toThrow(); 172 | // cannot remove non-existing task 173 | expect(() => timer.remove('non-existing-task')).toThrow(); 174 | 175 | timer.start(); 176 | }); 177 | 178 | test('ITaskTimerEvent', (done: Function) => { 179 | const timer = new TaskTimer(500); 180 | const taskComp: ITaskOptions = { 181 | id: 'check-task-completed', 182 | totalRuns: 2, 183 | callback: () => { } 184 | }; 185 | // second task will be added without name set. 186 | // it should be generated as "task2". 187 | const autoTaskID = 'task2'; 188 | const executedEvents: TaskTimer.Event[] = []; 189 | 190 | // pause should do nothing if not running 191 | expect(() => timer.pause()).not.toThrow(); 192 | 193 | timer 194 | .reset() 195 | .on(TaskTimer.Event.STARTED, (event: ITaskTimerEvent) => { 196 | executedEvents.push(event.name); 197 | expect(event.name).toEqual(TaskTimer.Event.STARTED); 198 | expect(event.source instanceof TaskTimer).toEqual(true); 199 | console.log('——» started'); 200 | }) 201 | .on(TaskTimer.Event.TICK, (event: ITaskTimerEvent) => { 202 | executedEvents.push(event.name); 203 | expect(event.name).toEqual(TaskTimer.Event.TICK); 204 | expect(event.source).toEqual(timer); 205 | expect(event.data).toBeUndefined(); 206 | console.log('——» tick', timer.tickCount); 207 | if (timer.tickCount === 2) { 208 | timer.add(taskComp); 209 | expect(timer.get(taskComp.id)).not.toBe(null); 210 | expect(timer.taskCount).toEqual(1); 211 | } 212 | if (timer.tickCount === 3) { 213 | timer.add(() => {}); 214 | // console.log((timer as any)._.tasks); 215 | expect(timer.get(autoTaskID)).not.toBe(null); 216 | expect(timer.taskCount).toEqual(2); 217 | } 218 | }) 219 | .on(TaskTimer.Event.TASK_ADDED, (event: ITaskTimerEvent) => { 220 | executedEvents.push(event.name); 221 | expect(event.name).toEqual(TaskTimer.Event.TASK_ADDED); 222 | expect(event.source instanceof TaskTimer).toEqual(true); 223 | expect(event.data instanceof Task).toEqual(true); 224 | const task = event.data as Task; 225 | console.log('——» taskAdded', JSON.stringify(task)); 226 | expect(task.tickInterval).toEqual(1); 227 | expect(typeof task.callback).toEqual('function'); 228 | if (task.id === taskComp.id) { 229 | expect(timer.tickCount).toEqual(2); 230 | expect(task.totalRuns).toEqual(taskComp.totalRuns); 231 | expect(timer.taskCount).toEqual(1); 232 | } else { 233 | expect(task.id).toEqual(autoTaskID); 234 | expect(timer.tickCount).toEqual(3); 235 | expect(task.totalRuns).toEqual(null); 236 | expect(timer.taskCount).toEqual(2); 237 | } 238 | }) 239 | .on(TaskTimer.Event.TASK, (event: ITaskTimerEvent) => { 240 | executedEvents.push(event.name); 241 | expect(event.name).toEqual(TaskTimer.Event.TASK); 242 | expect(event.source instanceof TaskTimer).toEqual(true); 243 | expect(event.data instanceof Task).toEqual(true); 244 | const task = event.data as Task; 245 | expect(task).toBeDefined(); 246 | console.log('——» task (executed)', JSON.stringify(task)); 247 | }) 248 | .on(TaskTimer.Event.TASK_COMPLETED, (event: ITaskTimerEvent) => { 249 | executedEvents.push(event.name); 250 | expect(event.name).toEqual(TaskTimer.Event.TASK_COMPLETED); 251 | expect(event.source instanceof TaskTimer).toEqual(true); 252 | expect(event.data instanceof Task).toEqual(true); 253 | expect(timer.taskCount).toEqual(2); 254 | const task = event.data as Task; 255 | expect(task).toBeDefined(); 256 | console.log('——» taskCompleted', JSON.stringify(task)); 257 | expect(task.id).toEqual(taskComp.id); 258 | timer.remove(autoTaskID); 259 | }) 260 | .on(TaskTimer.Event.TASK_REMOVED, (event: ITaskTimerEvent) => { 261 | executedEvents.push(event.name); 262 | expect(event.name).toEqual(TaskTimer.Event.TASK_REMOVED); 263 | expect(event.source instanceof TaskTimer).toEqual(true); 264 | expect(event.data instanceof Task).toEqual(true); 265 | expect(timer.taskCount).toEqual(1); 266 | const task = event.data as Task; 267 | expect(task).toBeDefined(); 268 | console.log('——» taskRemoved', JSON.stringify(task)); 269 | expect(task.id).toEqual(autoTaskID); 270 | expect(timer.get(autoTaskID)).toEqual(null); 271 | timer.pause(); 272 | }) 273 | .on(TaskTimer.Event.PAUSED, (event: ITaskTimerEvent) => { 274 | executedEvents.push(event.name); 275 | expect(event.name).toEqual(TaskTimer.Event.PAUSED); 276 | expect(event.source instanceof TaskTimer).toEqual(true); 277 | console.log('——» paused'); 278 | timer.resume(); 279 | }) 280 | .on(TaskTimer.Event.RESUMED, (event: ITaskTimerEvent) => { 281 | executedEvents.push(event.name); 282 | expect(event.name).toEqual(TaskTimer.Event.RESUMED); 283 | expect(event.source instanceof TaskTimer).toEqual(true); 284 | console.log('——» resumed'); 285 | timer.stop(); 286 | }) 287 | .on(TaskTimer.Event.STOPPED, (event: ITaskTimerEvent) => { 288 | executedEvents.push(event.name); 289 | expect(event.name).toEqual(TaskTimer.Event.STOPPED); 290 | expect(event.source instanceof TaskTimer).toEqual(true); 291 | console.log('——» stopped'); 292 | timer.reset(); 293 | }) 294 | .on(TaskTimer.Event.STOPPED, (event: ITaskTimerEvent) => { 295 | // second event listener for "stopped" 296 | executedEvents.push(event.name); 297 | expect(event.source instanceof TaskTimer).toEqual(true); 298 | }) 299 | .on(TaskTimer.Event.RESET, (event: ITaskTimerEvent) => { 300 | try { 301 | executedEvents.push(event.name); 302 | expect(event.name).toEqual(TaskTimer.Event.RESET); 303 | expect(event.source instanceof TaskTimer).toEqual(true); 304 | expect(timer.runCount).toEqual(2); 305 | console.log('——» reset'); 306 | checkEvents(); 307 | } catch (err) { 308 | console.log(err.stack || err); 309 | } 310 | done(); 311 | }); 312 | 313 | function checkEvents(): void { 314 | expect(executedEvents).toContain(TaskTimer.Event.STARTED); 315 | expect(timer.listenerCount(TaskTimer.Event.STARTED)).toEqual(1); 316 | 317 | expect(executedEvents).toContain(TaskTimer.Event.TICK); 318 | expect(timer.listenerCount(TaskTimer.Event.TICK)).toEqual(1); 319 | 320 | expect(executedEvents).toContain(TaskTimer.Event.TASK_ADDED); 321 | expect(timer.listenerCount(TaskTimer.Event.TASK_ADDED)).toEqual(1); 322 | 323 | expect(executedEvents).toContain(TaskTimer.Event.TASK); 324 | expect(timer.listenerCount(TaskTimer.Event.TASK)).toEqual(1); 325 | 326 | expect(executedEvents).toContain(TaskTimer.Event.TASK_COMPLETED); 327 | expect(timer.listenerCount(TaskTimer.Event.TASK_COMPLETED)).toEqual(1); 328 | 329 | expect(executedEvents).toContain(TaskTimer.Event.TASK_REMOVED); 330 | expect(timer.listenerCount(TaskTimer.Event.TASK_REMOVED)).toEqual(1); 331 | 332 | expect(executedEvents).toContain(TaskTimer.Event.PAUSED); 333 | expect(timer.listenerCount(TaskTimer.Event.PAUSED)).toEqual(1); 334 | 335 | expect(executedEvents).toContain(TaskTimer.Event.RESUMED); 336 | expect(timer.listenerCount(TaskTimer.Event.RESUMED)).toEqual(1); 337 | 338 | expect(executedEvents).toContain(TaskTimer.Event.STOPPED); 339 | expect(timer.listenerCount(TaskTimer.Event.STOPPED)).toEqual(2); 340 | 341 | expect(executedEvents).toContain(TaskTimer.Event.RESET); 342 | expect(timer.listenerCount(TaskTimer.Event.RESET)).toEqual(1); 343 | } 344 | 345 | timer.start(); 346 | expect(timer.state).toEqual(TaskTimer.State.RUNNING); 347 | console.log('——» running'); 348 | }); 349 | 350 | test('Task[], ITaskTimerOptions #stopOnCompleted', (done: any) => { 351 | const timer = new TaskTimer({ 352 | interval: 500, 353 | stopOnCompleted: true 354 | }); 355 | const tasks: ITaskOptions[] = [ 356 | { 357 | totalRuns: 2, 358 | callback(): void { } 359 | }, 360 | { 361 | totalRuns: 5, 362 | callback(): void { } 363 | } 364 | ]; 365 | timer.add(tasks); 366 | expect(timer.taskCount).toEqual(tasks.length); 367 | 368 | timer.on('tick', (event: ITaskTimerEvent) => { 369 | // console.log('timer.tickCount:', timer.tickCount, 'timer.taskRunCount:', timer.taskRunCount); 370 | if (timer.tickCount === 2) { 371 | expect(timer.state).toEqual(TaskTimer.State.RUNNING); 372 | expect(event.name).toEqual(TaskTimer.Event.TICK); 373 | expect(event.source instanceof TaskTimer).toEqual(true); 374 | } 375 | }); 376 | // tslint:disable:no-unnecessary-callback-wrapper 377 | timer.on(TaskTimer.Event.STOPPED, (event: ITaskTimerEvent) => { 378 | try { 379 | expect(timer.state).toEqual(TaskTimer.State.STOPPED); 380 | expect(event.name).toEqual(TaskTimer.Event.STOPPED); 381 | expect(event.source instanceof TaskTimer).toEqual(true); 382 | expect(timer.taskRunCount).toEqual(7); 383 | expect(timer.runCount).toEqual(1); 384 | } catch (err) { 385 | console.log(err.stack || err); 386 | } 387 | done(); 388 | }); 389 | // calling resume() instead of start() which should be equivalent if not 390 | // already paused. 391 | timer.resume(); 392 | }, 7000); // set a larger timeout for jest/jasmine 393 | 394 | test('ITaskOptions » startDate/stopDate cannot be the same', () => { 395 | const timer = new TaskTimer(500); 396 | const date = Date.now(); 397 | const add = () => timer.add({ 398 | id: 'task-date', 399 | startDate: new Date(date), 400 | stopDate: new Date(date), 401 | tickInterval: 1, 402 | callback(): void {} 403 | }); 404 | 405 | expect(add).toThrow(); 406 | }); 407 | 408 | test('ITaskOptions #startDate, #stopDate', (done: any) => { 409 | const timer = new TaskTimer({ 410 | interval: 500, 411 | stopOnCompleted: true 412 | }); 413 | const date = Date.now(); 414 | const startDate = new Date(date); 415 | const stopDate = new Date(date); 416 | startDate.setSeconds(startDate.getSeconds() + 5); 417 | stopDate.setSeconds(stopDate.getSeconds() + 10); 418 | timer.add({ 419 | id: 'task-date', 420 | startDate, 421 | stopDate, 422 | tickInterval: 1, 423 | callback(task: Task): void { 424 | if (task.currentRuns === 1) { 425 | expect(Date.now() - startDate.getTime()).toBeLessThan(1000); 426 | } 427 | console.log('currentRuns:', task.currentRuns, ' run date:', new Date().toISOString()); 428 | } 429 | }); 430 | 431 | const taskD = timer.get('task-date'); 432 | expect(taskD.completed).toEqual(false); 433 | expect(taskD.time.started).toEqual(0); 434 | expect(taskD.time.stopped).toEqual(0); 435 | expect(taskD.time.elapsed).toEqual(0); 436 | 437 | timer.on(TaskTimer.Event.TASK_COMPLETED, (event: ITaskTimerEvent) => { 438 | const task: Task = event.data as Task; 439 | try { 440 | expect(task instanceof Task).toEqual(true); 441 | expect(task.completed).toEqual(true); 442 | expect(timer.get('task-date').completed).toEqual(true); 443 | expect(task.currentRuns).toEqual(11); // one per 500ms in 5 seconds 444 | const elapsed = task.time.stopped - task.time.started; 445 | expect(task.time.elapsed).toEqual(elapsed); 446 | timer.stop(); 447 | } catch (err) { 448 | console.log(err.stack || err); 449 | } 450 | done(); 451 | }); 452 | timer.start(); 453 | console.log('timer started @', new Date().toISOString()); 454 | console.log('task startDate @', startDate.toISOString()); 455 | console.log('task stopDate @', stopDate.toISOString()); 456 | }, 15000); // set a larger timeout for jest/jasmine 457 | 458 | test('Task » #reset()', (done: any) => { 459 | const timer = new TaskTimer(500); 460 | const task = new Task({ 461 | id: 'task-to-reset', 462 | tickInterval: 1, 463 | totalRuns: 3, 464 | callback(): void { } 465 | }); 466 | timer.add(task); 467 | 468 | timer.on(TaskTimer.Event.TICK, (event: ITaskTimerEvent) => { 469 | try { 470 | if (timer.tickCount < 3) { 471 | console.log('will reset task on tick 3 » tick', timer.tickCount); 472 | } else if (timer.tickCount === 3) { 473 | expect(task.currentRuns).toEqual(2); 474 | expect(() => task.reset({ id: 'cannot-change' } as any)).toThrow(); 475 | task.reset(); 476 | console.log('task is reset!'); 477 | } else if (timer.tickCount === 5) { 478 | expect(task.currentRuns).toEqual(2); 479 | task.reset({ 480 | totalRuns: 2, 481 | callback(): void {} 482 | }); 483 | console.log('task is reset with options!'); 484 | } else { 485 | console.log('after task reset » tick', timer.tickCount); 486 | } 487 | } catch (err) { 488 | console.log(err.stack || err); 489 | } 490 | }); 491 | 492 | timer.on(TaskTimer.Event.TASK_COMPLETED, (event: ITaskTimerEvent) => { 493 | expect(task.totalRuns).toEqual(2); 494 | expect(task.currentRuns).toEqual(2); 495 | timer.stop(); 496 | }); 497 | 498 | timer.on(TaskTimer.Event.STOPPED, (event: ITaskTimerEvent) => { 499 | try { 500 | expect(timer.taskCount).toEqual(1); 501 | expect(timer.tickCount).toEqual(6); 502 | expect(timer.taskRunCount).toEqual(6); 503 | } catch (err) { 504 | console.log(err.stack || err); 505 | } 506 | done(); 507 | }); 508 | 509 | timer.start(); 510 | }, 10000); 511 | 512 | test('TaskCallback » sync, async + immediate, async (done()), promise', (done: any) => { 513 | const timer = new TaskTimer(500); 514 | const taskSync = new Task({ 515 | id: 'task-sync', 516 | tickInterval: 1, 517 | totalRuns: 1, 518 | callback(): void {} 519 | }); 520 | timer.add(taskSync); 521 | const taskAsync = new Task({ 522 | id: 'task-async', 523 | tickDelay: 2, 524 | tickInterval: 1, 525 | totalRuns: 1, 526 | callback(task: Task, taskDone: any): void { 527 | setTimeout(() => taskDone(), 100); 528 | } 529 | }); 530 | timer.add(taskAsync); 531 | const taskAsyncImmediate = new Task({ 532 | id: 'task-sync-immediate', 533 | tickDelay: 2, 534 | tickInterval: 1, 535 | totalRuns: 1, 536 | immediate: true, 537 | callback(): void { } 538 | }); 539 | timer.add(taskAsyncImmediate); 540 | const taskPromise = new Task({ 541 | id: 'task-promise', 542 | tickDelay: 3, 543 | tickInterval: 1, 544 | totalRuns: 1, 545 | callback(): Promise { 546 | return getPromise(true); 547 | } 548 | }); 549 | timer.add(taskPromise); 550 | const taskDisabled = new Task({ 551 | id: 'task-disabled', 552 | enabled: false, 553 | tickDelay: 2, 554 | tickInterval: 1, 555 | totalRuns: 1, 556 | callback(): void {} 557 | }); 558 | timer.add(taskDisabled); 559 | 560 | let firstAsync = null; 561 | timer.on(TaskTimer.Event.TASK_COMPLETED, (event: ITaskTimerEvent) => { 562 | try { 563 | const task: Task = event.data; 564 | console.log(task.id, 'completed on tick #', timer.tickCount); 565 | if (timer.tickCount === 1) { 566 | expect(task.id).toEqual(taskSync.id); 567 | } else if (timer.tickCount === 3) { 568 | // taskAsyncImmediate should run/complete before taskAsync 569 | if (!firstAsync) { 570 | expect(task.id).toEqual(taskAsyncImmediate.id); 571 | firstAsync = task; 572 | } else { 573 | expect(task.id).toEqual(taskAsync.id); 574 | } 575 | } else if (timer.tickCount === 4) { 576 | expect(task.id).toEqual(taskPromise.id); 577 | // taskDisabled should not have run 578 | expect(timer.taskRunCount).toEqual(4); 579 | timer.stop(); 580 | done(); 581 | } 582 | } catch (err) { 583 | console.log(err.stack || err); 584 | } 585 | }); 586 | timer.start(); 587 | }); 588 | 589 | test('Task » ~toJSON()', () => { 590 | const task = new Task({ 591 | id: 'task-json', 592 | tickInterval: 1, 593 | totalRuns: 3, 594 | callback(): void { } 595 | }); 596 | const o = JSON.parse(JSON.stringify(task)); 597 | // console.log(o); 598 | expect(o.id).toEqual(task.id); 599 | expect(o.enabled).toEqual(task.enabled); 600 | expect(o.tickDelay).toEqual(task.tickDelay); 601 | expect(o.tickInterval).toEqual(task.tickInterval); 602 | expect(o.totalRuns).toEqual(task.totalRuns); 603 | expect(o.currentRuns).toEqual(task.currentRuns); 604 | expect(o.immediate).toEqual(task.immediate); 605 | expect(o.removeOnCompleted).toEqual(task.removeOnCompleted); 606 | expect(o.startDate).toEqual(null); 607 | expect(o.stopDate).toEqual(null); 608 | expect(o.callback).toBeUndefined(); 609 | }); 610 | 611 | test('TaskTimer.EventType.TASK_ERROR (sync)', (done: any) => { 612 | const timer = new TaskTimer(500); 613 | const taskError = new Error('task error'); 614 | const task = new Task({ 615 | id: 'task-error', 616 | tickInterval: 1, 617 | totalRuns: 1, 618 | callback(): void { 619 | throw taskError; 620 | } 621 | }); 622 | timer.add(task); 623 | timer.on(TaskTimer.Event.TASK_ERROR, (event: ITaskTimerEvent) => { 624 | try { 625 | expect(event.name).toEqual(TaskTimer.Event.TASK_ERROR); 626 | expect(event.source).toEqual(task); 627 | expect(event.error).toEqual(taskError); 628 | timer.stop(); 629 | } catch (err) { 630 | console.log(err.stack || err); 631 | } 632 | done(); 633 | }); 634 | timer.start(); 635 | }); 636 | 637 | test('TaskTimer.EventType.TASK_ERROR (promise)', (done: any) => { 638 | const timer = new TaskTimer(500); 639 | const taskError = new Error('task error'); 640 | const task = new Task({ 641 | id: 'task-error', 642 | tickInterval: 1, 643 | totalRuns: 1, 644 | callback(): Promise { 645 | return getPromise(taskError); 646 | } 647 | }); 648 | timer.add(task); 649 | timer.on(TaskTimer.Event.TASK_ERROR, (event: ITaskTimerEvent) => { 650 | try { 651 | expect(event.name).toEqual(TaskTimer.Event.TASK_ERROR); 652 | expect(event.source).toEqual(task); 653 | expect(event.error).toEqual(taskError); 654 | timer.stop(); 655 | } catch (err) { 656 | console.log(err.stack || err); 657 | } 658 | done(); 659 | }); 660 | timer.start(); 661 | }); 662 | 663 | test('ITaskTimerOptions » #precision = false', (done: any) => { 664 | const interval = 500; 665 | const timer = new TaskTimer({ 666 | interval, 667 | precision: false, 668 | stopOnCompleted: true 669 | }); 670 | timer.add({ 671 | tickInterval: 1, 672 | totalRuns: 3, 673 | callback(task: Task): void { 674 | console.log('tick @', task.currentRuns, ':', timer.time.elapsed); 675 | block(interval + 100); 676 | const mod = timer.time.elapsed % interval; 677 | expect(mod <= 20 || mod >= 20).toEqual(true); 678 | } 679 | }); 680 | // tslint:disable:no-unnecessary-callback-wrapper 681 | timer.on(TaskTimer.Event.STOPPED, () => { 682 | // console.log('stop event fired'); 683 | done(); 684 | }); 685 | timer.start(); 686 | }, 5000); // set a larger timeout for jest/jasmine 687 | 688 | test('precision: catch up with setImmediate()', (done: any) => { 689 | expect.assertions(isCI ? 2 : 4); 690 | 691 | const elapsedList = []; 692 | const totalRuns = 10; 693 | const interval = 400; 694 | const timer = new TaskTimer({ 695 | interval, 696 | precision: true, 697 | stopOnCompleted: true 698 | }); 699 | let start; 700 | let cn = 0; 701 | timer.add({ 702 | tickInterval: 1, 703 | totalRuns, 704 | removeOnCompleted: true, 705 | callback(): void { 706 | // if (cn >= totalRuns) done(); 707 | cn++; 708 | const diff = Date.now() - start; 709 | elapsedList.push(diff); 710 | console.log('tick @', cn, ':', diff); 711 | // block each run with a sync operation (longer than the 712 | // interval) except 3rd and 6th so that these 2 use 713 | // setImmediate() to catch up. 714 | if ([3, 6].indexOf(cn) === -1) block(interval + 50); 715 | } 716 | }); 717 | timer.on(TaskTimer.Event.STOPPED, (event: ITaskTimerEvent) => { 718 | console.log('stopped, task count:', timer.taskCount); 719 | console.log(elapsedList); 720 | expect(timer.taskCount).toEqual(1); 721 | done(); 722 | }); 723 | timer.on(TaskTimer.Event.COMPLETED, (event: ITaskTimerEvent) => { 724 | try { 725 | console.log('completed, run count:', timer.taskRunCount); 726 | expect(timer.taskRunCount).toEqual(totalRuns); 727 | // below somehow fail on CI. but coverage is still 100% when 728 | // these tests are disabled. 729 | if (!isCI) { 730 | expect(elapsedList[3] % (interval * 4)).toBeLessThan(10); 731 | expect(elapsedList[6] % (interval * 7)).toBeLessThan(10); 732 | } 733 | } catch (err) { 734 | console.log(err.stack || err); 735 | } 736 | }); 737 | start = Date.now(); 738 | timer.start(); 739 | }, 20000); // set a larger timeout for jest/jasmine 740 | 741 | }); 742 | 743 | function block(time: number = 100): void { 744 | const start = Date.now(); 745 | while (Date.now() - start < time) {} 746 | } 747 | 748 | function getPromise(value: any, delay: number = 100): Promise { 749 | return new Promise((resolve, reject) => { 750 | setTimeout(() => { 751 | if (value instanceof Error) { 752 | reject(value); 753 | } else { 754 | resolve(value); 755 | } 756 | }, delay); 757 | }); 758 | } 759 | -------------------------------------------------------------------------------- /test/node/utils.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-empty */ 2 | 3 | import { utils } from '../../src/utils'; 4 | 5 | describe('utils (Node/TypeScript)', () => { 6 | 7 | test('type()', () => { 8 | expect(utils.type(null)).toEqual('null'); 9 | expect(utils.type(undefined)).toEqual('undefined'); 10 | expect(utils.type(true)).toEqual('boolean'); 11 | expect(utils.type([])).toEqual('array'); 12 | expect(utils.type({})).toEqual('object'); 13 | expect(utils.type(1)).toEqual('number'); 14 | expect(utils.type('1')).toEqual('string'); 15 | expect(utils.type(new Date())).toEqual('date'); 16 | }); 17 | 18 | test('isset(), ensureArray()', () => { 19 | expect(utils.isset(null)).toEqual(false); 20 | expect(utils.isset(undefined)).toEqual(false); 21 | expect(utils.isset(false)).toEqual(true); 22 | expect(utils.isset(0)).toEqual(true); 23 | 24 | expect(utils.type(utils.ensureArray(1))).toEqual('array'); 25 | expect(utils.type(utils.ensureArray(''))).toEqual('array'); 26 | expect(utils.ensureArray([])).toEqual([]); 27 | expect(utils.ensureArray(1)).toEqual([1]); 28 | expect(utils.ensureArray(null)).toEqual([]); 29 | expect(utils.ensureArray(undefined)).toEqual([]); 30 | expect(utils.ensureArray(false)).toEqual([false]); 31 | }); 32 | 33 | test('getNumber(), getBool()', () => { 34 | expect(utils.getNumber(5, 1, 0)).toEqual(5); 35 | expect(utils.getNumber(0, 1, 0)).toEqual(1); 36 | expect(utils.getNumber(null, 1, 3)).toEqual(3); 37 | expect(utils.getBool(false, true)).toEqual(false); 38 | expect(utils.getBool(true, false)).toEqual(true); 39 | expect(utils.getBool(null, true)).toEqual(true); 40 | expect(utils.getBool(undefined, true)).toEqual(true); 41 | }); 42 | 43 | test('setImmediate()', (done: any) => { 44 | let ref = utils.setImmediate(() => { 45 | try { 46 | expect(ref).toBeDefined(); 47 | utils.clearImmediate(ref); 48 | ref = undefined; 49 | expect(ref).toBeUndefined(); 50 | } catch (err) { 51 | console.log(err.stack || err); 52 | } 53 | done(); 54 | }, 500); 55 | }); 56 | 57 | test('isPromise()', (done: any) => { 58 | expect(utils.isPromise(Promise.resolve(false))).toEqual(true); 59 | expect(utils.isPromise(getPromise(done))).toEqual(true); 60 | }); 61 | 62 | }); 63 | 64 | function getPromise(cb: any): Promise { 65 | return new Promise((resolve, reject) => { 66 | setTimeout(() => { 67 | resolve(true); 68 | cb(); 69 | }, 500); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowJs": false, // cannot be true when decleration is true 5 | "allowUnreachableCode": false, 6 | "allowUnusedLabels": false, 7 | "alwaysStrict": true, 8 | "charset": "utf8", 9 | "checkJs": false, 10 | "declaration": true, 11 | "declarationDir": "./lib", 12 | "diagnostics": false, 13 | "disableSizeLimit": false, 14 | "downlevelIteration": false, 15 | "emitBOM": false, 16 | "emitDeclarationOnly": false, 17 | "emitDecoratorMetadata": true, 18 | "esModuleInterop": true, 19 | "experimentalDecorators": true, 20 | "forceConsistentCasingInFileNames": false, 21 | "importHelpers": false, 22 | "inlineSourceMap": false, 23 | "inlineSources": false, 24 | "isolatedModules": false, 25 | // "jsx": "preserve", 26 | // "jsxFactory": "React.createElement", 27 | "lib": [ 28 | "es5", 29 | "es2015", 30 | "dom" 31 | ], 32 | "listEmittedFiles": false, 33 | "listFiles": false, 34 | "locale": "en-us", 35 | "maxNodeModuleJsDepth": 0, 36 | "module": "commonjs", 37 | "moduleResolution": "Node", 38 | "noEmit": false, 39 | "noEmitHelpers": false, 40 | "noEmitOnError": false, 41 | "noErrorTruncation": false, 42 | "noFallthroughCasesInSwitch": true, 43 | "noImplicitAny": false, 44 | "noImplicitReturns": true, 45 | "noImplicitThis": false, 46 | "noImplicitUseStrict": false, 47 | "noLib": false, 48 | "noResolve": false, 49 | "noStrictGenericChecks": false, 50 | "noUnusedLocals": true, 51 | "noUnusedParameters": false, 52 | "outDir": "./lib", 53 | "preserveConstEnums": false, 54 | "preserveSymlinks": false, 55 | "preserveWatchOutput": false, 56 | "pretty": true, 57 | "removeComments": false, 58 | "skipLibCheck": false, 59 | "sourceMap": false, 60 | "strict": false, 61 | "strictFunctionTypes": false, 62 | "strictPropertyInitialization": false, 63 | "strictNullChecks": false, 64 | "stripInternal": false, 65 | "suppressExcessPropertyErrors": false, 66 | "suppressImplicitAnyIndexErrors": true, 67 | "target": "es5", 68 | "traceResolution": false, 69 | "typeRoots": [ 70 | "./node_modules/@types", 71 | "./node_modules" 72 | ], 73 | "types": [ 74 | "node", 75 | "jest" 76 | // "./node_modules/eventemitter3/index.d.ts" 77 | ] 78 | }, 79 | "include": [ 80 | "./src/**/*.ts", 81 | "./node_modules/eventemitter3/*.ts" 82 | ], 83 | "exclude": [ 84 | "./node_modules", 85 | "./lib", 86 | "./dev", 87 | "./docs", 88 | "./test", 89 | "./tasks", 90 | "./backup" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | 4 | // ---------------------------- 5 | // TypeScript-specific 6 | // ---------------------------- 7 | 8 | "adjacent-overload-signatures": true, 9 | "ban-types": [true, ["Object", "Use {} instead."], ["String"]], 10 | "member-access": [true, "no-public"], 11 | "member-ordering": [true, {"order": "fields-first"}], 12 | "no-any": false, 13 | "no-empty-interface": false, 14 | "no-import-side-effect": true, 15 | "no-inferrable-types": [true, "ignore-params"], // "ignore-properties" 16 | "no-internal-module": true, 17 | "no-magic-numbers": [false], 18 | // "no-namespace": [true, "allow-declarations"], 19 | "no-non-null-assertion": true, 20 | "no-reference": true, 21 | // "no-unnecessary-type-assertion": true, // rule requires type information. 22 | "no-var-requires": true, 23 | "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], 24 | "prefer-for-of": true, 25 | // "promise-function-async": true, // rule requires type information 26 | "typedef": [true, "call-signature", "parameter", "member-variable-declaration"], 27 | "typedef-whitespace": [ 28 | true, 29 | { 30 | "call-signature": "nospace", 31 | "index-signature": "nospace", 32 | "parameter": "nospace", 33 | "property-declaration": "nospace", 34 | "variable-declaration": "nospace" 35 | }, 36 | { 37 | "call-signature": "onespace", 38 | "index-signature": "onespace", 39 | "parameter": "onespace", 40 | "property-declaration": "onespace", 41 | "variable-declaration": "onespace" 42 | } 43 | ], 44 | "unified-signatures": true, 45 | 46 | // ---------------------------- 47 | // Functionality 48 | // ---------------------------- 49 | 50 | // "await-promise": [true, "Thenable"], // rule requires type information 51 | // "ban": [], 52 | "curly": [true, "ignore-same-line"], 53 | "forin": true, 54 | // "import-blacklist": [true, "rxjs", "lodash"], 55 | "label-position": true, 56 | "no-arg": true, 57 | "no-bitwise": true, 58 | "no-conditional-assignment": true, 59 | // "no-console": [true, "log", "error"], 60 | "no-construct": true, 61 | "no-debugger": true, 62 | "no-duplicate-super": true, 63 | "no-duplicate-variable": [true, "check-parameters"], 64 | "no-empty": [true, "allow-empty-catch"], 65 | "no-eval": true, 66 | // "no-floating-promises": [true, "JQueryPromise"], // rule requires type information 67 | // "no-for-in-array": true, // rule requires type information 68 | // "no-inferred-empty-object-type": true, // rule requires type information 69 | "no-invalid-template-strings": true, 70 | "no-invalid-this": [true, "check-function-in-method"], 71 | "no-misused-new": true, 72 | "no-null-keyword": false, 73 | "no-object-literal-type-assertion": true, 74 | "no-shadowed-variable": true, 75 | "no-sparse-arrays": true, 76 | "no-string-literal": true, 77 | "no-string-throw": true, 78 | "no-switch-case-fall-through": true, 79 | "no-this-assignment": [true, {"allowed-names": ["^self$"], "allow-destructuring": true}], 80 | // "no-unbound-method": [true, "ignore-static"], // rule requires type information 81 | // "no-unsafe-any": true, // rule requires type information 82 | "no-unsafe-finally": true, 83 | "no-unused-expression": [true, "allow-fast-null-checks"], 84 | // "no-unused-variable": [true, {"ignore-pattern": "^_"}], // rule requires type information 85 | // "no-use-before-declare": true, // rule requires type information 86 | "no-var-keyword": true, 87 | // "no-void-expression": [true, "ignore-arrow-function-shorthand"], // rule requires type information 88 | "prefer-conditional-expression": true, 89 | "prefer-object-spread": true, 90 | "radix": true, 91 | // "restrict-plus-operands": true, // rule requires type information 92 | // "strict-boolean-expressions": [true, "allow-boolean-or-undefined"], // rule requires type information 93 | // "strict-type-predicates": true, // does not work without --strictNullChecks // rule requires type information 94 | "switch-default": true, 95 | "triple-equals": true, 96 | // "use-default-type-parameter": true, // rule requires type information 97 | "use-isnan": true, 98 | 99 | // ---------------------------- 100 | // Maintainability 101 | // ---------------------------- 102 | 103 | "cyclomatic-complexity": [true, 30], 104 | // "deprecation": true, // rule requires type information 105 | "eofline": true, 106 | "linebreak-style": [true, "LF"], 107 | "max-classes-per-file": [true, 1], 108 | "max-file-line-count": [true, 750], 109 | "max-line-length": [true, 135], 110 | "no-default-export": true, 111 | "no-mergeable-namespace": true, 112 | "no-require-imports": true, 113 | "object-literal-sort-keys": [false, "ignore-case"], 114 | "prefer-const": [true, {"destructuring": "all"}], 115 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], 116 | 117 | // ---------------------------- 118 | // Style 119 | // ---------------------------- 120 | 121 | "align": [true, "parameters", "statements"], 122 | // "array-type": [true, "array"], 123 | "arrow-parens": [true, "ban-single-arg-parens"], 124 | "arrow-return-shorthand": [true], 125 | "binary-expression-operand-order": true, 126 | "callable-types": true, 127 | "class-name": true, 128 | "comment-format": [true, "check-space", {"ignore-words": ["TODO", "HACK", "NOTE"]}], 129 | // "completed-docs": [true, "enums", "enum-members", "functions", "methods", "properties", "interfaces", "classes"], // rule requires type information 130 | "encoding": true, 131 | // "file-header": [false], 132 | "import-spacing": true, 133 | "interface-name": [true, "always-prefix"], 134 | "interface-over-type-literal": true, 135 | "jsdoc-format": true, 136 | // "match-default-export-name": true, // rule requires type information 137 | // "newline-before-return": true, 138 | "new-parens": true, 139 | "no-angle-bracket-type-assertion": true, 140 | // "no-boolean-literal-compare": true, // rule requires type information 141 | "no-consecutive-blank-lines": [true, 1], 142 | "no-irregular-whitespace": true, 143 | "no-parameter-properties": true, 144 | "no-reference-import": true, 145 | "no-trailing-whitespace": [true, "ignore-jsdoc"], 146 | "no-unnecessary-callback-wrapper": true, 147 | "no-unnecessary-initializer": true, 148 | // "no-unnecessary-qualifier": true, // rule requires type information 149 | "number-literal-format": true, 150 | "object-literal-key-quotes": [true, "as-needed"], 151 | "object-literal-shorthand": true, 152 | "one-line": [false], 153 | "one-variable-per-declaration": [true, "ignore-for-loop"], 154 | "ordered-imports": [true], 155 | "prefer-function-over-method": [true, "allow-protected", "allow-public"], 156 | "prefer-method-signature": true, 157 | "prefer-switch": [true, {"min-cases": 4}], 158 | // "prefer-template": [true, "allow-single-concat"], 159 | "quotemark": [true, "single", "avoid-escape", "avoid-template"], 160 | // "return-undefined": true, // rule requires type information 161 | "semicolon": [true, "always"], 162 | "space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}], 163 | "switch-final-break": true, 164 | "type-literal-delimiter": true, 165 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 166 | "whitespace": [ 167 | true, 168 | "check-branch", 169 | "check-decl", 170 | "check-operator", 171 | "check-separator" 172 | ] 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 4 | 5 | const libraryName = 'tasktimer'; 6 | const libPath = path.resolve(__dirname, 'lib'); 7 | const srcPath = path.resolve(__dirname, 'src'); 8 | const nodeModules = path.resolve(__dirname, 'node_modules'); 9 | const publicPath = 'lib/'; 10 | 11 | module.exports = env => { 12 | 13 | const config = { 14 | // turn off NodeStuffPlugin and NodeSourcePlugin plugins. Otherwise 15 | // objects like `process` are mocked or polyfilled. 16 | node: false, // !!! IMPORTANT 17 | 18 | context: __dirname, // to automatically find tsconfig.json 19 | cache: false, 20 | entry: path.join(srcPath, 'index.ts'), 21 | devtool: 'inline-source-map', 22 | output: { 23 | library: libraryName, 24 | filename: libraryName.toLowerCase() + '.js' 25 | }, 26 | module: { 27 | rules: [{ 28 | test: /\.tsx?$/, 29 | loader: 'ts-loader', 30 | include: [ 31 | srcPath, 32 | path.join(nodeModules, 'eventemitter3') 33 | ], 34 | options: { 35 | compilerOptions: { 36 | // override dir for webpack context (we're already in libPath) 37 | outDir: './' 38 | }, 39 | // By default, ts-loader will not compile .ts files in 40 | // node_modules. You should not need to recompile .ts files 41 | // there, but if you really want to, use this option. Note 42 | // that this option acts as a whitelist - any modules you 43 | // desire to import must be included in the "files" or 44 | // "include" block of your project's tsconfig.json. 45 | allowTsInNodeModules: true 46 | // IMPORTANT! Since ForkTsCheckerWebpackPlugin is already 47 | // doing the type-checking. here we use transpileOnly mode 48 | // to speed-up compilation. 49 | // transpileOnly: true 50 | } 51 | }] 52 | }, 53 | resolve: { 54 | modules: [srcPath, nodeModules], 55 | extensions: ['.ts', '.tsx', '.js'] 56 | }, 57 | // Configure the console output. 58 | stats: { 59 | colors: true, 60 | modules: false, 61 | reasons: true, 62 | // suppress "export not found" warnings about re-exported types 63 | warningsFilter: /export .* was not found in/ 64 | }, 65 | plugins: [ 66 | new ForkTsCheckerWebpackPlugin() 67 | ], 68 | optimization: { 69 | minimizer: [] 70 | } 71 | }; 72 | 73 | if (env.WEBPACK_OUT === 'coverage') { 74 | Object.assign(config.output, { 75 | filename: '.' + libraryName + '.cov.js', 76 | path: libPath, 77 | libraryTarget: 'commonjs2', 78 | umdNamedDefine: false 79 | }); 80 | } else { 81 | 82 | // production & development 83 | Object.assign(config.output, { 84 | path: libPath, 85 | publicPath, 86 | libraryTarget: 'umd', 87 | umdNamedDefine: true, 88 | // this is to get rid of 'window is not defined' error. 89 | // https://stackoverflow.com/a/49119917/112731 90 | globalObject: 'this' 91 | }); 92 | 93 | if (env.WEBPACK_OUT === 'production') { 94 | config.devtool = 'source-map'; 95 | config.output.filename = libraryName.toLowerCase() + '.min.js'; 96 | config.optimization.minimizer.push(new UglifyJsPlugin({ 97 | test: /\.js$/, 98 | sourceMap: true, 99 | uglifyOptions: { 100 | ie8: false, 101 | ecma: 5, 102 | output: { 103 | comments: false, 104 | beautify: false 105 | }, 106 | compress: true, 107 | warnings: true 108 | } 109 | })); 110 | } 111 | } 112 | 113 | return config; 114 | }; 115 | --------------------------------------------------------------------------------