├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src └── tiny-timer.ts ├── test-ts.ts ├── test.js ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | dist/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - lts/* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.4.0](https://github.com/mathiasvr/tiny-timer/tree/v1.4.0) (2019-07-31) 4 | [Full Changelog](https://github.com/mathiasvr/tiny-timer/compare/v1.3.0...v1.4.0) 5 | 6 | **Merged pull requests:** 7 | 8 | - Change source code to TypeScript [\#8](https://github.com/mathiasvr/tiny-timer/pull/8) ([mathiasvr](https://github.com/mathiasvr)) 9 | - Update README.md [\#4](https://github.com/mathiasvr/tiny-timer/pull/4) ([jbbpatel94](https://github.com/jbbpatel94)) 10 | - Update README.md [\#3](https://github.com/mathiasvr/tiny-timer/pull/3) ([jbbpatel94](https://github.com/jbbpatel94)) 11 | 12 | ## [v1.3.0](https://github.com/mathiasvr/tiny-timer/tree/v1.3.0) (2018-11-27) 13 | [Full Changelog](https://github.com/mathiasvr/tiny-timer/compare/v1.2.0...v1.3.0) 14 | 15 | **Merged pull requests:** 16 | 17 | - emitting an event when status of timer changes [\#2](https://github.com/mathiasvr/tiny-timer/pull/2) ([jbbpatel94](https://github.com/jbbpatel94)) 18 | 19 | ## [v1.2.0](https://github.com/mathiasvr/tiny-timer/tree/v1.2.0) (2018-06-19) 20 | [Full Changelog](https://github.com/mathiasvr/tiny-timer/compare/v1.1.1...v1.2.0) 21 | 22 | **Merged pull requests:** 23 | 24 | - Build process + random updates [\#1](https://github.com/mathiasvr/tiny-timer/pull/1) ([marinko-peso](https://github.com/marinko-peso)) 25 | 26 | ## [v1.1.1](https://github.com/mathiasvr/tiny-timer/tree/v1.1.1) (2016-10-11) 27 | [Full Changelog](https://github.com/mathiasvr/tiny-timer/compare/v1.1.0...v1.1.1) 28 | 29 | ## [v1.1.0](https://github.com/mathiasvr/tiny-timer/tree/v1.1.0) (2016-10-06) 30 | [Full Changelog](https://github.com/mathiasvr/tiny-timer/compare/v1.0.2...v1.1.0) 31 | 32 | ## [v1.0.2](https://github.com/mathiasvr/tiny-timer/tree/v1.0.2) (2016-09-24) 33 | [Full Changelog](https://github.com/mathiasvr/tiny-timer/compare/v1.0.1...v1.0.2) 34 | 35 | ## [v1.0.1](https://github.com/mathiasvr/tiny-timer/tree/v1.0.1) (2016-09-23) 36 | 37 | 38 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mathias Rasmussen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tiny-timer 2 | 3 | [![npm](https://img.shields.io/npm/v/tiny-timer?style=for-the-badge)](https://npm.im/tiny-timer) 4 | [![Build Status](https://img.shields.io/travis/com/mathiasvr/tiny-timer?style=for-the-badge)](https://travis-ci.com/mathiasvr/tiny-timer) 5 | ![Dependency status](https://img.shields.io/librariesio/release/npm/tiny-timer?style=for-the-badge) 6 | ![downloads](https://img.shields.io/npm/dt/tiny-timer?style=for-the-badge) 7 | [![license](https://img.shields.io/:license-MIT-blue?style=for-the-badge)](https://mvr.mit-license.org) 8 | 9 | Small countdown timer and stopwatch module. 10 | 11 | ## Installation 12 | npm: 13 | ```shell 14 | $ npm install tiny-timer 15 | ``` 16 | Yarn: 17 | ```shell 18 | $ yarn add tiny-timer 19 | ``` 20 | 21 | ## Example 22 | ```javascript 23 | const Timer = require('tiny-timer') 24 | 25 | const timer = new Timer() 26 | 27 | timer.on('tick', (ms) => console.log('tick', ms)) 28 | timer.on('done', () => console.log('done!')) 29 | timer.on('statusChanged', (status) => console.log('status:', status)) 30 | 31 | timer.start(5000) // run for 5 seconds 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### `timer = new Timer({ interval: 1000, stopwatch: false })` 37 | Optionally set the refresh `interval` in ms, or `stopwatch` mode instead of countdown. 38 | 39 | ### `timer.start(duration [, interval])` { 40 | Starts timer running for a `duration` specified in ms. 41 | Optionally override the default refresh `interval` in ms. 42 | 43 | ### `timer.stop()` 44 | Stops timer. 45 | 46 | ### `timer.pause()` 47 | Pauses timer. 48 | 49 | ### `timer.resume()` 50 | Resumes timer. 51 | 52 | ## Events 53 | 54 | ### `timer.on('tick', (ms) => {})` 55 | Event emitted every `interval` with the current time in ms. 56 | 57 | ### `timer.on('done', () => {})` 58 | Event emitted when the timer reaches the `duration` set by calling `timer.start()`. 59 | 60 | ### `timer.on('statusChanged', (status) => {})` 61 | Event emitted when the timer status changes. 62 | 63 | ## Properties 64 | 65 | ### `timer.time` 66 | Gets the current time in ms. 67 | 68 | ### `timer.duration` 69 | Gets the total `duration` the timer is running for in ms. 70 | 71 | ### `timer.status` 72 | Gets the current status of the timer as a string: `running`, `paused` or `stopped`. 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-timer", 3 | "version": "1.6.0", 4 | "description": "Small countdown timer and stopwatch module.", 5 | "source": "src/tiny-timer.ts", 6 | "main": "dist/tiny-timer.js", 7 | "module": "dist/tiny-timer.module.js", 8 | "types": "dist/tiny-timer.d.ts", 9 | "scripts": { 10 | "build": "rimraf dist/* && microbundle --target=node --format=es,cjs", 11 | "test": "npm run lint && tape test.js && npm run test-ts", 12 | "test-ts": "ts-node -O '{\"esModuleInterop\": true, \"module\": \"commonjs\"}' test-ts.ts", 13 | "lint": "tslint --project .", 14 | "pre-tslint": "prettier-tslint fix ./src/tiny-timer.ts", 15 | "update-contributors": "git-authors-cli", 16 | "prepare": "npm run build", 17 | "prepublishOnly": "npm test" 18 | }, 19 | "keywords": [ 20 | "countdown", 21 | "stopwatch", 22 | "timer" 23 | ], 24 | "author": "Mathias Rasmussen ", 25 | "contributors": [ 26 | "Mathias Rasmussen ", 27 | "Jaisa Ram ", 28 | "Marinko Pešo ", 29 | "Michael Ishola ", 30 | "Yrobot " 31 | ], 32 | "license": "MIT", 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/mathiasvr/tiny-timer.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/mathiasvr/tiny-timer/issues" 39 | }, 40 | "homepage": "https://github.com/mathiasvr/tiny-timer#readme", 41 | "devDependencies": { 42 | "@types/node": "^14.14.22", 43 | "@types/tape": "^4.13.0", 44 | "git-authors-cli": "^1.0.33", 45 | "microbundle": "^0.13.0", 46 | "prettier": "^2.2.1", 47 | "prettier-tslint": "^0.4.2", 48 | "rimraf": "^3.0.2", 49 | "tape": "^5.1.1", 50 | "ts-node": "^9.1.1", 51 | "tslint": "^6.1.3", 52 | "typescript": "^4.1.3" 53 | }, 54 | "dependencies": { 55 | "mitt": "^2.1.0" 56 | }, 57 | "files": [ 58 | "dist" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/tiny-timer.ts: -------------------------------------------------------------------------------- 1 | import mitt, { EventType, Handler } from 'mitt' 2 | 3 | type Status = 'running' | 'paused' | 'stopped' 4 | 5 | class Timer { 6 | private _interval: number 7 | private _stopwatch: boolean 8 | private _duration: number = 0 9 | private _endTime: number = 0 10 | private _pauseTime: number = 0 11 | private _status: Status = 'stopped' 12 | private _timeoutID?: NodeJS.Timeout 13 | private _emitter = mitt() 14 | 15 | constructor ({ interval = 1000, stopwatch = false } = {}) { 16 | this._interval = interval 17 | this._stopwatch = stopwatch 18 | } 19 | 20 | public start (duration: number, interval?: number) { 21 | if (this.status !== 'stopped') return 22 | if (duration == null) { 23 | throw new TypeError('Must provide duration parameter') 24 | } 25 | this._duration = duration 26 | this._endTime = Date.now() + duration 27 | this._changeStatus('running') 28 | this._emitter.emit('tick', this._stopwatch ? 0 : this._duration) 29 | this._timeoutID = setInterval(this.tick, interval || this._interval) 30 | } 31 | 32 | public stop () { 33 | if (this._timeoutID) clearInterval(this._timeoutID) 34 | this._changeStatus('stopped') 35 | } 36 | 37 | public pause () { 38 | if (this.status !== 'running') return 39 | this._pauseTime = Date.now() 40 | this._changeStatus('paused') 41 | } 42 | 43 | public resume () { 44 | if (this.status !== 'paused') return 45 | this._endTime += Date.now() - this._pauseTime 46 | this._pauseTime = 0 47 | this._changeStatus('running') 48 | } 49 | 50 | private _changeStatus (status: Status) { 51 | this._status = status 52 | this._emitter.emit('statusChanged', this.status) 53 | } 54 | 55 | private tick = () => { 56 | if (this.status === 'paused') return 57 | if (Date.now() >= this._endTime) { 58 | this.stop() 59 | this._emitter.emit('tick', this._stopwatch ? this._duration : 0) 60 | this._emitter.emit('done') 61 | } else { 62 | this._emitter.emit('tick', this.time) 63 | } 64 | } 65 | 66 | get time () { 67 | if (this.status === 'stopped') return 0 68 | const time = this.status === 'paused' ? this._pauseTime : Date.now() 69 | const left = this._endTime - time 70 | return this._stopwatch ? this._duration - left : left 71 | } 72 | 73 | get duration () { 74 | return this._duration 75 | } 76 | 77 | get status () { 78 | return this._status 79 | } 80 | 81 | public on (eventName: EventType, handler: Handler) { 82 | this._emitter.on(eventName, handler) 83 | } 84 | 85 | public off (eventName: EventType, handler: Handler) { 86 | this._emitter.off(eventName, handler) 87 | } 88 | } 89 | 90 | export default Timer 91 | -------------------------------------------------------------------------------- /test-ts.ts: -------------------------------------------------------------------------------- 1 | // NOTE: This is mostly the same as test.js, 2 | // but is included to test TypeScript type errors. 3 | 4 | import test from 'tape' 5 | import Timer from '.' 6 | 7 | test('countdown ticks', { timeout: 500 }, (t) => { 8 | const timer = new Timer({ interval: 10 }) 9 | let lastms = 51 10 | 11 | timer.on('tick', (ms) => { 12 | if (lastms === 51) t.equal(ms, 50, 'first update should be 50') 13 | t.ok(ms < lastms, 'time decreasing') 14 | lastms = ms 15 | }) 16 | 17 | timer.on('done', () => { 18 | t.equal(lastms, 0, 'last update should be 0') 19 | t.end() 20 | }) 21 | 22 | timer.start(50) 23 | }) 24 | 25 | test('stopwatch ticks', { timeout: 500 }, (t) => { 26 | const timer = new Timer({ interval: 10, stopwatch: true }) 27 | let lastms = -1 28 | 29 | timer.on('tick', (ms) => { 30 | if (lastms === -1) t.equal(ms, 0, 'first update should be 0') 31 | t.ok(ms > lastms, 'time increasing') 32 | lastms = ms 33 | }) 34 | 35 | timer.on('done', () => { 36 | t.equal(lastms, 50, 'last update should be 50') 37 | t.end() 38 | }) 39 | 40 | timer.start(50) 41 | }) 42 | 43 | test('stop', (t) => { 44 | const timer = new Timer({ interval: 10 }) 45 | 46 | timer.on('done', () => t.fail()) 47 | 48 | setTimeout(() => { 49 | t.equal(timer.status, 'stopped') 50 | t.end() 51 | }, 100) 52 | 53 | timer.start(50) 54 | t.equal(timer.status, 'running') 55 | timer.stop() 56 | }) 57 | 58 | test('pause and resume', (t) => { 59 | const timer = new Timer({ interval: 10 }) 60 | const startTime = Date.now() 61 | 62 | timer.on('done', () => { 63 | t.ok(Date.now() - startTime > 100, 'paused for at least 100ms') 64 | t.end() 65 | }) 66 | 67 | setTimeout(() => { 68 | t.equal(timer.status, 'paused') 69 | timer.resume() 70 | }, 100) 71 | 72 | timer.start(50) 73 | t.equal(timer.status, 'running') 74 | timer.pause() 75 | }) 76 | 77 | test('state transition', (t) => { 78 | const timer = new Timer({ interval: 10 }) 79 | t.equal(timer.status, 'stopped') 80 | timer.stop() 81 | t.equal(timer.status, 'stopped') 82 | timer.pause() 83 | t.equal(timer.status, 'stopped') 84 | timer.resume() 85 | t.equal(timer.status, 'stopped') 86 | timer.start(20) 87 | t.equal(timer.status, 'running') 88 | timer.pause() 89 | t.equal(timer.status, 'paused') 90 | timer.resume() 91 | t.equal(timer.status, 'running') 92 | timer.stop() 93 | t.equal(timer.status, 'stopped') 94 | timer.start(20) 95 | t.equal(timer.status, 'running') 96 | timer.pause() 97 | timer.stop() 98 | t.equal(timer.status, 'stopped') 99 | timer.start(20) 100 | timer.on('done', () => { 101 | t.equal(timer.status, 'stopped') 102 | t.end() 103 | }) 104 | }) 105 | 106 | test('duration property', (t) => { 107 | const timer = new Timer({ interval: 10 }) 108 | timer.on('done', () => { 109 | t.equal(timer.duration, 50, 'correct last duration') 110 | t.end() 111 | }) 112 | 113 | timer.start(50) 114 | t.equal(timer.duration, 50, 'correct duration') 115 | }) 116 | 117 | test('time property', (t) => { 118 | const run = (stopwatch: boolean) => { 119 | const timer = new Timer({ interval: 10, stopwatch }) 120 | timer.on('tick', (ms) => { 121 | const time = timer.time 122 | // TODO: last ms and time is not equal in stopwatch mode 123 | // because we stop the timer before calling tick to ensure 124 | // that .time won't be less than 0 or greater than duration 125 | if (stopwatch && ms === 50) return 126 | t.ok(time > ms - 5 && time < ms + 5, 'time should be around the ms param') 127 | }) 128 | 129 | timer.on('done', () => { 130 | t.equal(timer.time, 0) 131 | if (stopwatch) t.end() 132 | }) 133 | 134 | t.equal(timer.time, 0) 135 | timer.start(50) 136 | 137 | const rtime = timer.time 138 | timer.pause() 139 | const ptime = timer.time 140 | 141 | t.equal(rtime, ptime) 142 | 143 | setTimeout(() => { 144 | t.equal(ptime, timer.time) 145 | timer.resume() 146 | }, 20) 147 | } 148 | 149 | run(false) 150 | run(true) 151 | }) 152 | 153 | test('override interval', (t) => { 154 | const timer = new Timer({ interval: 1000 }) 155 | let ticks = 0 156 | 157 | timer.on('tick', (ms) => ticks++) 158 | 159 | timer.on('done', (ms) => { 160 | t.ok(ticks > 5, 'should do extra tick events') 161 | t.end() 162 | }) 163 | 164 | timer.start(100, 10) 165 | }) 166 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const Timer = require('.') 3 | 4 | test('countdown ticks', { timeout: 500 }, function (t) { 5 | const timer = new Timer({ interval: 10 }) 6 | let lastms = 51 7 | 8 | timer.on('tick', (ms) => { 9 | if (lastms === 51) t.equal(ms, 50, 'first update should be 50') 10 | t.ok(ms < lastms, 'time decreasing') 11 | lastms = ms 12 | }) 13 | 14 | timer.on('done', () => { 15 | t.equal(lastms, 0, 'last update should be 0') 16 | t.end() 17 | }) 18 | 19 | timer.start(50) 20 | }) 21 | 22 | test('stopwatch ticks', { timeout: 500 }, function (t) { 23 | const timer = new Timer({ interval: 10, stopwatch: true }) 24 | let lastms = -1 25 | 26 | timer.on('tick', (ms) => { 27 | if (lastms === -1) t.equal(ms, 0, 'first update should be 0') 28 | t.ok(ms > lastms, 'time increasing') 29 | lastms = ms 30 | }) 31 | 32 | timer.on('done', () => { 33 | t.equal(lastms, 50, 'last update should be 50') 34 | t.end() 35 | }) 36 | 37 | timer.start(50) 38 | }) 39 | 40 | test('stop', function (t) { 41 | const timer = new Timer({ interval: 10 }) 42 | 43 | timer.on('done', () => t.fail()) 44 | 45 | setTimeout(() => { 46 | t.equal(timer.status, 'stopped') 47 | t.end() 48 | }, 100) 49 | 50 | timer.start(50) 51 | t.equal(timer.status, 'running') 52 | timer.stop() 53 | }) 54 | 55 | test('pause and resume', function (t) { 56 | const timer = new Timer({ interval: 10 }) 57 | const startTime = Date.now() 58 | 59 | timer.on('done', () => { 60 | t.ok(Date.now() - startTime > 100, 'paused for at least 100ms') 61 | t.end() 62 | }) 63 | 64 | setTimeout(() => { 65 | t.equal(timer.status, 'paused') 66 | timer.resume() 67 | }, 100) 68 | 69 | timer.start(50) 70 | t.equal(timer.status, 'running') 71 | timer.pause() 72 | }) 73 | 74 | test('state transition', function (t) { 75 | const timer = new Timer({ interval: 10 }) 76 | t.equal(timer.status, 'stopped') 77 | timer.stop() 78 | t.equal(timer.status, 'stopped') 79 | timer.pause() 80 | t.equal(timer.status, 'stopped') 81 | timer.resume() 82 | t.equal(timer.status, 'stopped') 83 | timer.start(20) 84 | t.equal(timer.status, 'running') 85 | timer.pause() 86 | t.equal(timer.status, 'paused') 87 | timer.resume() 88 | t.equal(timer.status, 'running') 89 | timer.stop() 90 | t.equal(timer.status, 'stopped') 91 | timer.start(20) 92 | t.equal(timer.status, 'running') 93 | timer.pause() 94 | timer.stop() 95 | t.equal(timer.status, 'stopped') 96 | timer.start(20) 97 | timer.on('done', () => { 98 | t.equal(timer.status, 'stopped') 99 | t.end() 100 | }) 101 | }) 102 | 103 | test('duration property', function (t) { 104 | const timer = new Timer({ interval: 10 }) 105 | timer.on('done', () => { 106 | t.equal(timer.duration, 50, 'correct last duration') 107 | t.end() 108 | }) 109 | 110 | timer.start(50) 111 | t.equal(timer.duration, 50, 'correct duration') 112 | }) 113 | 114 | test('time property', function (t) { 115 | const run = function (stopwatch) { 116 | const timer = new Timer({ interval: 10, stopwatch: stopwatch }) 117 | timer.on('tick', (ms) => { 118 | const time = timer.time 119 | // TODO: last ms and time is not equal in stopwatch mode 120 | // because we stop the timer before calling tick to ensure 121 | // that .time won't be less than 0 or greater than duration 122 | if (stopwatch && ms === 50) return 123 | t.ok(time > ms - 5 && time < ms + 5, 'time should be around the ms param') 124 | }) 125 | 126 | timer.on('done', () => { 127 | t.equal(timer.time, 0) 128 | if (stopwatch) t.end() 129 | }) 130 | 131 | t.equal(timer.time, 0) 132 | timer.start(50) 133 | 134 | const rtime = timer.time 135 | timer.pause() 136 | const ptime = timer.time 137 | 138 | t.equal(rtime, ptime) 139 | 140 | setTimeout(() => { 141 | t.equal(ptime, timer.time) 142 | timer.resume() 143 | }, 20) 144 | } 145 | 146 | run(false) 147 | run(true) 148 | }) 149 | 150 | test('override interval', function (t) { 151 | const timer = new Timer({ interval: 1000 }) 152 | let ticks = 0 153 | 154 | timer.on('tick', (ms) => ticks++) 155 | 156 | timer.on('done', (ms) => { 157 | t.ok(ticks > 5, 'should do extra tick events') 158 | t.end() 159 | }) 160 | 161 | timer.start(100, 10) 162 | }) 163 | 164 | test('argument errors', function (t) { 165 | const timer = new Timer() 166 | t.throws(() => timer.start(), TypeError, 'Throw TypeError') 167 | t.end() 168 | }) 169 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es2017", 5 | "module": "es2015", 6 | "declaration": true, 7 | "outDir": "dist-ts/", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "moduleResolution": "Node" 13 | }, 14 | "exclude": [ 15 | "node_modules/" 16 | ], 17 | "include": [ 18 | "./src/**/*" 19 | ], 20 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "rules": { 7 | "semicolon": [true, "never"], 8 | "quotemark": [true, "single"], 9 | "curly": [true, "ignore-same-line"], 10 | "space-before-function-paren": [true, "always"], 11 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"] 12 | } 13 | } --------------------------------------------------------------------------------