workwatch
Version:
A Linux terminal program for honest worktime tracking and billing.
157 lines (150 loc) • 6.02 kB
JavaScript
/**
* 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;