UNPKG

workwatch

Version:

A Linux terminal program for honest worktime tracking and billing.

157 lines (150 loc) 6.02 kB
/** * This file is part of the WorkWatch, a Linux terminal program for honest * worktime tracking and billing. * * Copyright (C) 2020-2025 by Artur Rutkowski * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * WorkWatch is beeing developped and maintained by Artur (locust) Rutkwoski * <locust@mailbox.org> */ /** * This is a file defining a Measure object responsible for precise and * accurate time measurement. It measures milliseconds for stable and reliable * counting of minutes the user is doing a work. Object is handled by events. */ const process = require("node:process"); const {performance} = require("node:perf_hooks"); const {EventEmitter} = require("node:events"); const timeData = require("./utils/time-data.js"); // Creates a Measure objects binding some event handlers. It is a closure // due to making the object controllable by events. function createMeasure( tickHandlerCallback, stopHandlerCallback, endHandlerCallback = null ) { // contains all hidden methods for object-specific tasks. const measureData = { initialize() { // The Measure object measures time in milliseconds. All time variables // except "minutes" hold values in that manner. this.started = Math.floor(performance.timeOrigin); this.measured = timeData.timeMS - this.started; this.minutes = 0; this.endTime = timeData.absoluteEndTimeMS; // Holds a timestamp of last full minute measured in milliseconds. // It is used in rounding the count down to fully measured and finished // minutes. this.lastMinuteTimestamp = 0; // Holds the greatest timer interval deviation ever happend. The value is in MS. this.deviation = 0; this.timer = null; }, // Starts the measurement emitting "start" event providing the Node's // real start time which causes the measurement from the very beginning // of the Node.js process. start() { // If there is less than a minute to the end time (23:59), no measure // has sense to perform. if ((this.endTime - timeData.timeMS) < timeData.constants.MINUTE_IN_MS) { measureObject.emit("error", new Error("Too little time left to perform the measurement!")); } else { // Proceed normally. const stopOnSignal = () => { this.stop(); }; process.on("SIGINT", stopOnSignal); process.on("SIGTERM", stopOnSignal); process.on("SIGHUP", stopOnSignal); process.on("SIGQUIT", stopOnSignal); this.timer = setInterval( () => { this.measure(); }, timeData.constants.TIMER_INTERVAL_MS ); measureObject.emit("start", timeData.correctTimeShort(this.started)); } }, // Stops the measurement emitting 'stop' event providing measured minutes // and the time when measure has been stopped. stop() { clearInterval(this.timer); measureObject.emit("stop", this.minutes, timeData.correctTimeShort( this.lastMinuteTimestamp || timeData.timeMS )); }, // Performs all calculations for checking the measurement accuracy and // stops the measurement when the end time has been reached emitting // 'end' event. During the measurement it emits the 'tick' event providing // currently measured minutes. measure() { let currentlyMeasured = timeData.timeMS - this.started; let currentDeviation = currentlyMeasured - this.measured; this.deviation = ( currentDeviation > this.deviation )? currentDeviation : this.deviation; this.measured = currentlyMeasured; let currentMinutes = Math.floor(this.measured / timeData.constants.MINUTE_IN_MS); let nearEndMS = Math.abs(this.endTime - timeData.timeMS); if (this.minutes < currentMinutes) { this.minutes = currentMinutes; this.lastMinuteTimestamp = timeData.timeMS; measureObject.emit("tick", this.minutes); } if ( currentDeviation < this.deviation && nearEndMS <= currentDeviation ) { this.minutes = Math.ceil(this.measured / timeData.constants.MINUTE_IN_MS); this.stop(); measureObject.emit("end"); } } }; // Measure object itself. function Measure() { // Inheritance from EventEmitter. EventEmitter.call(this); Object.setPrototypeOf(Measure.prototype, EventEmitter.prototype); // Initialize measureData. measureData.initialize(); // Discovering event listeners binding to detect 'start' event. this.on("newListener", (eventName, listenerFunction) => { // Run the Measure object when there is first listener for the // 'start' event. if ( eventName === "start" && this.listenerCount(eventName) === 0 ) { // Defer the start due to assertion that the 'start' event has // a listener bound. setImmediate(() => { measureData.start(); }); } }); } // Preparing object to return. const measureObject = new Measure(); // Don't bind the 'start' event to make it possible to run the measurement // when it is needed. measureObject.on("tick", tickHandlerCallback); measureObject.on("stop", stopHandlerCallback); if (endHandlerCallback !== null) { measureObject.on("end", endHandlerCallback); } return measureObject; } module.exports = createMeasure;