UNPKG

overtimer

Version:

Mission critical updateable javascript timer. It can handle overtimes also limits.

576 lines (491 loc) 16.2 kB
/** * Enum for Overtimer states. * @type {{CREATED: number, WAITING: number, RUNNING: number, PAUSED: number, STOPPED: number}} */ Overtimer.STATES = { CREATED: 0, WAITING: 1, RUNNING: 2, PAUSED: 3, STOPPED: 4 } /** * Global timer object for performance. */ Overtimer.global = { callbacks: [], timer: null, updateMs: 1, lastId: 0, /** * Joins the global timer list * @param callback {function} Callback function will trigger ~ every ms * @return {Number} Unique timer id for leave. */ join(callback) { let found = Overtimer.global.callbacks.find(f => f.callback === callback) if (typeof found === 'undefined') { Overtimer.global.lastId += 1 Overtimer.global.callbacks.push({callback, id: Overtimer.global.lastId}) } else { return found.id } if (Overtimer.global.timer === null) Overtimer.global.timer = setInterval(Overtimer.global.tick, Overtimer.global.updateMs) return Overtimer.global.lastId }, /** * Leaves from global timer list * @param id {number} Auto generated id from join for leave. */ leave(id) { Overtimer.global.callbacks = Overtimer.global.callbacks.filter(c => c.id !== id) if (Overtimer.global.callbacks.length === 0 && Overtimer.global.timer !== null) { clearInterval(Overtimer.global.timer) Overtimer.global.timer = null } }, /** * Callback trigger function for every ms */ tick() { Overtimer.global.callbacks.forEach((cb) => { cb.callback() }) } } /** * Overtimer constructor. * @param duration {Number} Duration of timer * @param opts {Object} Overtimer options * @param onFinish {Function} Shorthand for on('finish') * @constructor */ function Overtimer(duration = 1000, opts = {}, onFinish = null) { let finishEvent = null let defaults = { duration, overtimeLimit: duration, overtimeBump: duration, poll: 100, delay: 0, repeat: 1, debug: false, start: true, } this.eventHandlers = { 'start': [], 'tick': [], 'stop': [], 'pause': [], 'resume': [], 'delaystart': [], 'delayend': [], 'bump': [], 'repeat': [], 'finish': [], 'update': [], 'poll': [] } if (typeof opts === 'object') { this.options = Object.assign({}, defaults, opts) } else { if (typeof opts === 'function') { finishEvent = opts } this.options = Object.assign({}, defaults) } if (typeof onFinish === 'function') finishEvent = onFinish let durationError = false if (typeof duration !== 'number') { this.log('Duration must be number value.', 1000) durationError = true } else if (duration <= 0) { this.log('Duration must be bigger than 0.', 1001) durationError = true } if( durationError ) { this.options.duration = 1000 this.options.overtimeLimit = 1000 this.options.overtimeBump = 1000 } // Properties this.version = '0.1.6' this.globalTimerId = null this.state = Overtimer.STATES.CREATED this.createdAt = Date.now() this.startedAt = -1 this.delayStartedAt = -1 this.delayEndedAt = -1 this.repeatedAt = -1 this.tickedAt = -1 this.stoppedAt = -1 this.finishedAt = -1 this.pausedAt = -1 this.resumedAt = -1 this.bumpedAt = -1 this.pausedTime = -1 this.delayedTime = -1 this.overTime = -1 this.elapsedTime = -1 this.remainingTime = -1 this.totalDelayedTime = -1 this.totalElapsedTime = -1 this.totalRemainingTime = -1 this.currentRepeat = -1 this.repeatDuration = -1 this.repeatDurationWithDelay = -1 this.totalDuration = -1 this.totalDurationWithDelay = -1 this.currentRepeatPercentWithDelay = -1 this.currentRepeatPercent = -1 this.totalPercentWithDelay = -1 this.totalPercent = -1 this.timesUpdatedAt = -1 this.lastPollAt = -1 if (typeof finishEvent === 'function') this.on('finish', finishEvent) if (this.options.start) this.start() } /** * Logs errors if debug enabled * @param msg {String} Message will be displayed * @param code {Number} Code will be displayed */ Overtimer.prototype.log = function (msg = 'Unexcepted error.', code = -1) { if (this.options.debug) console.log(`${code !== -1 ? '( ' + code.toString() + ' ): ' : ''} ${msg}`) } /** * Registers callback to event. * @param eventName {String} Selected event name from available events. * @param callback {Function} Function to be triggered when the event occurs. * @return {Boolean} true if succeeded, false if not. */ Overtimer.prototype.on = function (eventName, callback) { if (typeof eventName !== 'string') { this.log('Event name must be string.', 1002) return false } else if (eventName.length < 1) { this.log('Event name length be bigger than 0.', 1003) return false } else if (typeof this.eventHandlers[eventName] === 'undefined') { this.log('Event name not registered!', 1004) return false } else if (typeof callback !== 'function') { this.log('Callback is not function!', 1005) return false } this.eventHandlers[eventName].push(callback) return true } /** * Removes all callbacks or spesific function from event * @param eventName {String} Selected event name from available events. * @param func {Function} Function to be removed. * @return {boolean} true if succeeded, false if not. */ Overtimer.prototype.off = function (eventName, func = null) { if (typeof eventName !== 'string') { this.log('Event name must be string.', 1006) return false } else if (eventName.length < 1) { this.log('Event name length be bigger than 0.', 1007) return false } else if (typeof this.eventHandlers[eventName] === 'undefined') { this.log('Event name not registered!', 1008) return false } if (typeof func === 'function') this.eventHandlers[eventName] = this.eventHandlers[eventName].filter(f => f !== func) else this.eventHandlers[eventName] = [] return true } /** * Triggers registered event * @param eventName {string} Event name you want to trigger * @param payload {object} payload for trigger. can be array, or can be object * @return {boolean} Returns true if succeeded, false if not */ Overtimer.prototype.trigger = function (eventName, ...payload) { if (typeof eventName !== 'string') { this.log('Event name must be string.', 1009) return false } else if (eventName.length < 1) { this.log("Event name's length must bigger than 1.", 1010) return false } else if (typeof this.eventHandlers[eventName.toLowerCase()] === 'undefined' || !Array.isArray(this.eventHandlers[eventName.toLowerCase()])) { this.log('Event not found in list.', 1011) return false } this.eventHandlers[eventName.toLowerCase()].forEach((evt) => { evt.call(this, ...payload) }) return true } /** * Joins main interval for updates. */ Overtimer.prototype.joinToMainInterval = function () { this.globalTimerId = Overtimer.global.join(this.tickMainInterval.bind(this)) } /** * All time updates happens in this function. */ Overtimer.prototype.tickMainInterval = function () { let now = Date.now(), diff = now - this.timesUpdatedAt if (this.state === Overtimer.STATES.RUNNING) { this.elapsedTime += diff this.remainingTime -= diff this.totalElapsedTime += diff this.totalRemainingTime -= diff this.currentRepeatPercentWithDelay = parseFloat((this.elapsedTime + this.delayedTime) / this.repeatDurationWithDelay * 100) this.totalPercentWithDelay = parseFloat((this.totalElapsedTime + this.totalDelayedTime) / this.totalDurationWithDelay * 100) this.currentRepeatPercent = parseFloat(this.elapsedTime / this.repeatDuration * 100) this.totalPercent = parseFloat(this.totalElapsedTime / this.totalDuration * 100) } else if (this.state === Overtimer.STATES.PAUSED) { this.pausedTime += diff } else if (this.state === Overtimer.STATES.WAITING) { this.delayedTime += diff this.totalDelayedTime += diff this.currentRepeatPercentWithDelay = parseFloat((this.elapsedTime + this.delayedTime) / this.repeatDurationWithDelay * 100) this.totalPercentWithDelay = parseFloat((this.totalElapsedTime + this.totalDelayedTime) / this.totalDurationWithDelay * 100) if (this.delayedTime >= this.options.delay) this.endDelay() } this.timesUpdatedAt = now this.trigger('update') if (this.lastPollAt + this.options.poll < Date.now()) { this.trigger('poll') this.lastPollAt = Date.now() } if (this.remainingTime < 1) this.tick() } /** * Leaves from main interval */ Overtimer.prototype.leaveFromMainInterval = function () { Overtimer.global.leave(this.globalTimerId) this.globalTimerId = null } /** * Increases the current timer's duration to within the maximum timeout limit. * @param customValue {number} Custom duration for bump duration. OvertimeBump option will use if not specified. * @return {boolean} true if bump success, false if not */ Overtimer.prototype.bump = function (customValue = -1) { if (this.state === Overtimer.STATES.STOPPED || this.state === Overtimer.STATES.CREATED) { this.log('Can\'t use overtime bump on stopped or created state.', 1011) return false } else if (typeof this.options.overtimeLimit !== 'number' || this.options.overtimeLimit <= 0) { this.log('Can\'t use overtime bump when overtime limit below 0.', 1012) return false } let mustBumpFor = (typeof customValue === 'number' && customValue > 0) ? customValue : this.options.overtimeBump if (typeof mustBumpFor !== 'number' || mustBumpFor <= 0) { this.log('Bump value is not valid! Must be number and bigger than 0 for bump:', 1013) this.log(mustBumpFor, 1014) return false } let maxBump = this.options.overtimeLimit - this.remainingTime if (maxBump < 0) { this.log('Timer not reached the overtime limit yet.', 1015) return false } else if (mustBumpFor < maxBump) { this.remainingTime += mustBumpFor this.totalRemainingTime += mustBumpFor this.overTime += mustBumpFor this.repeatDuration += mustBumpFor this.repeatDurationWithDelay += mustBumpFor this.totalDuration += mustBumpFor this.totalDurationWithDelay += mustBumpFor this.bumpedAt = Date.now() this.trigger('bump', mustBumpFor, this.remainingTime) } else { this.remainingTime += maxBump this.totalRemainingTime += maxBump this.overTime += maxBump this.repeatDuration += maxBump this.repeatDurationWithDelay += maxBump this.totalDuration += maxBump this.totalDurationWithDelay += maxBump this.bumpedAt = Date.now() this.trigger('bump', maxBump, this.remainingTime) } return true } /** * Starts the timer * @return {boolean} true if succeeded, false if not */ Overtimer.prototype.start = function () { if (this.state === Overtimer.STATES.RUNNING || this.state === Overtimer.STATES.WAITING) { this.log('Timer is already started.', 1016) return false } let totalDuration = this.options.duration * this.options.repeat if (this.options.delay > 0) { this.state = Overtimer.STATES.WAITING this.delayedTime = 0 this.totalDelayedTime = 0 this.delayStartedAt = Date.now() this.totalDurationWithDelay = totalDuration + (this.options.repeat * this.options.delay) this.repeatDurationWithDelay = this.options.duration + this.options.delay } else { this.state = Overtimer.STATES.RUNNING this.totalDurationWithDelay = totalDuration this.repeatDurationWithDelay = this.options.duration } this.startedAt = Date.now() this.elapsedTime = 0 this.totalElapsedTime = 0 this.pausedTime = 0 this.overTime = 0 this.remainingTime = this.options.duration this.repeatDuration = this.options.duration this.totalRemainingTime = totalDuration this.totalDuration = totalDuration this.currentRepeat = 1 this.currentRepeatPercentWithDelay = 0 this.currentRepeatPercent = 0 this.totalPercentWithDelay = 0 this.totalPercent = 0 this.timesUpdatedAt = Date.now() this.lastPollAt = Date.now() this.joinToMainInterval() this.trigger('start') if (this.options.delay > 0) this.trigger('delaystart') return true } /** * Pauses the timer * @return {boolean} true if succeeded, false if not */ Overtimer.prototype.pause = function () { if (this.state !== Overtimer.STATES.RUNNING && this.state !== Overtimer.STATES.WAITING) { this.log("Can't pause when timer not running.", 1017) return false } this.state = Overtimer.STATES.PAUSED this.pausedAt = Date.now() return true } /** * Ends delay immediately * @return {boolean} return true if succeeded, false if not */ Overtimer.prototype.endDelay = function () { if (this.state !== Overtimer.STATES.WAITING) { this.log("Can't end delay when timer not waiting.", 1018) return false } else if( typeof this.options.delay !== 'number' || this.options.delay <= 0 ) { this.log("Can't end delay when delay option not number or delay below 0.", 1019) return false } let remainingDelay = this.options.delay - this.delayedTime this.delayEndedAt = Date.now() this.state = Overtimer.STATES.RUNNING if( remainingDelay > 0 ) { this.totalDuration -= remainingDelay this.totalDurationWithDelay -= remainingDelay this.repeatDuration -= remainingDelay this.repeatDurationWithDelay -= remainingDelay } this.trigger('delayend') return true } /** * Resumes the paused timer * @return {boolean} true if succeeded, false if not */ Overtimer.prototype.resume = function () { if (this.state !== Overtimer.STATES.PAUSED) { this.log("Can't resume when timer not paused.", 1020) return false } if (this.options.delay > 0 && this.delayedTime < this.options.delay) this.state = Overtimer.STATES.WAITING else this.state = Overtimer.STATES.RUNNING this.resumedAt = Date.now() return true } /** * Repeats the loop * @return {boolean} Returns true if succeeded, false if not */ Overtimer.prototype.repeat = function () { if (this.state !== Overtimer.STATES.RUNNING && this.state !== Overtimer.STATES.WAITING) { this.log("Can't repeat when timer not running.", 1021) return false } this.totalDuration -= this.remainingTime this.totalDurationWithDelay -= this.remainingTime this.currentRepeat += 1 this.elapsedTime = 0 this.remainingTime = this.options.duration this.repeatedAt = Date.now() this.totalRemainingTime = ( this.options.repeat * this.options.duration ) - ((this.currentRepeat - 1) * this.options.duration) this.currentRepeatPercentWithDelay = 0 this.currentRepeatPercent = 0 if (this.options.delay > 0) { this.delayedTime = 0 this.state = Overtimer.STATES.WAITING } else this.state = Overtimer.STATES.RUNNING this.timesUpdatedAt = Date.now() this.trigger('repeat') if (this.options.delay > 0) this.trigger('delaystart') return true } /** * Works when timer tick time comes. */ Overtimer.prototype.tick = function () { this.tickedAt = Date.now() this.trigger('tick') if (this.currentRepeat < this.options.repeat) this.repeat() else { this.finishedAt = Date.now() this.totalRemainingTime = 0 this.remainingTime = 0 this.stoppedAt = Date.now() this.totalPercentWithDelay = 100 this.totalPercent = 100 this.currentRepeatPercent = 100 this.currentRepeatPercentWithDelay = 100 this.trigger('update') this.trigger('poll') this.trigger('finish') this.stop() } } /** * Stops the timer * @return {boolean} Returns true if succeeded, false if not */ Overtimer.prototype.stop = function () { if (this.state === Overtimer.STATES.STOPPED) { this.log('Timer is already stopped.', 1022) return false } this.leaveFromMainInterval() this.state = Overtimer.STATES.STOPPED this.stoppedAt = Date.now() this.trigger('stop') return true } if (typeof module !== 'undefined') module.exports = Overtimer if (typeof window !== 'undefined') window.Overtimer = Overtimer