summaryrefslogtreecommitdiffstats
path: root/server/uptime-calculator.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/uptime-calculator.js')
-rw-r--r--server/uptime-calculator.js865
1 files changed, 865 insertions, 0 deletions
diff --git a/server/uptime-calculator.js b/server/uptime-calculator.js
new file mode 100644
index 0000000..71d1d45
--- /dev/null
+++ b/server/uptime-calculator.js
@@ -0,0 +1,865 @@
+const dayjs = require("dayjs");
+const { UP, MAINTENANCE, DOWN, PENDING } = require("../src/util");
+const { LimitQueue } = require("./utils/limit-queue");
+const { log } = require("../src/util");
+const { R } = require("redbean-node");
+
+/**
+ * Calculates the uptime of a monitor.
+ */
+class UptimeCalculator {
+ /**
+ * @private
+ * @type {{string:UptimeCalculator}}
+ */
+ static list = {};
+
+ /**
+ * For testing purposes, we can set the current date to a specific date.
+ * @type {dayjs.Dayjs}
+ */
+ static currentDate = null;
+
+ /**
+ * monitorID the id of the monitor
+ * @type {number}
+ */
+ monitorID;
+
+ /**
+ * Recent 24-hour uptime, each item is a 1-minute interval
+ * Key: {number} DivisionKey
+ * @type {LimitQueue<number,string>}
+ */
+ minutelyUptimeDataList = new LimitQueue(24 * 60);
+
+ /**
+ * Recent 30-day uptime, each item is a 1-hour interval
+ * Key: {number} DivisionKey
+ * @type {LimitQueue<number,string>}
+ */
+ hourlyUptimeDataList = new LimitQueue(30 * 24);
+
+ /**
+ * Daily uptime data,
+ * Key: {number} DailyKey
+ */
+ dailyUptimeDataList = new LimitQueue(365);
+
+ lastUptimeData = null;
+ lastHourlyUptimeData = null;
+ lastDailyUptimeData = null;
+
+ lastDailyStatBean = null;
+ lastHourlyStatBean = null;
+ lastMinutelyStatBean = null;
+
+ /**
+ * For migration purposes.
+ * @type {boolean}
+ */
+ migrationMode = false;
+
+ statMinutelyKeepHour = 24;
+ statHourlyKeepDay = 30;
+
+ /**
+ * Get the uptime calculator for a monitor
+ * Initializes and returns the monitor if it does not exist
+ * @param {number} monitorID the id of the monitor
+ * @returns {Promise<UptimeCalculator>} UptimeCalculator
+ */
+ static async getUptimeCalculator(monitorID) {
+ if (!monitorID) {
+ throw new Error("Monitor ID is required");
+ }
+
+ if (!UptimeCalculator.list[monitorID]) {
+ UptimeCalculator.list[monitorID] = new UptimeCalculator();
+ await UptimeCalculator.list[monitorID].init(monitorID);
+ }
+ return UptimeCalculator.list[monitorID];
+ }
+
+ /**
+ * Remove a monitor from the list
+ * @param {number} monitorID the id of the monitor
+ * @returns {Promise<void>}
+ */
+ static async remove(monitorID) {
+ delete UptimeCalculator.list[monitorID];
+ }
+
+ /**
+ *
+ */
+ constructor() {
+ if (process.env.TEST_BACKEND) {
+ // Override the getCurrentDate() method to return a specific date
+ // Only for testing
+ this.getCurrentDate = () => {
+ if (UptimeCalculator.currentDate) {
+ return UptimeCalculator.currentDate;
+ } else {
+ return dayjs.utc();
+ }
+ };
+ }
+ }
+
+ /**
+ * Initialize the uptime calculator for a monitor
+ * @param {number} monitorID the id of the monitor
+ * @returns {Promise<void>}
+ */
+ async init(monitorID) {
+ this.monitorID = monitorID;
+
+ let now = this.getCurrentDate();
+
+ // Load minutely data from database (recent 24 hours only)
+ let minutelyStatBeans = await R.find("stat_minutely", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
+ monitorID,
+ this.getMinutelyKey(now.subtract(24, "hour")),
+ ]);
+
+ for (let bean of minutelyStatBeans) {
+ let data = {
+ up: bean.up,
+ down: bean.down,
+ avgPing: bean.ping,
+ minPing: bean.pingMin,
+ maxPing: bean.pingMax,
+ };
+
+ if (bean.extras != null) {
+ data = {
+ ...data,
+ ...JSON.parse(bean.extras),
+ };
+ }
+
+ let key = bean.timestamp;
+ this.minutelyUptimeDataList.push(key, data);
+ }
+
+ // Load hourly data from database (recent 30 days only)
+ let hourlyStatBeans = await R.find("stat_hourly", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
+ monitorID,
+ this.getHourlyKey(now.subtract(30, "day")),
+ ]);
+
+ for (let bean of hourlyStatBeans) {
+ let data = {
+ up: bean.up,
+ down: bean.down,
+ avgPing: bean.ping,
+ minPing: bean.pingMin,
+ maxPing: bean.pingMax,
+ };
+
+ if (bean.extras != null) {
+ data = {
+ ...data,
+ ...JSON.parse(bean.extras),
+ };
+ }
+
+ this.hourlyUptimeDataList.push(bean.timestamp, data);
+ }
+
+ // Load daily data from database (recent 365 days only)
+ let dailyStatBeans = await R.find("stat_daily", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
+ monitorID,
+ this.getDailyKey(now.subtract(365, "day")),
+ ]);
+
+ for (let bean of dailyStatBeans) {
+ let data = {
+ up: bean.up,
+ down: bean.down,
+ avgPing: bean.ping,
+ minPing: bean.pingMin,
+ maxPing: bean.pingMax,
+ };
+
+ if (bean.extras != null) {
+ data = {
+ ...data,
+ ...JSON.parse(bean.extras),
+ };
+ }
+
+ this.dailyUptimeDataList.push(bean.timestamp, data);
+ }
+ }
+
+ /**
+ * @param {number} status status
+ * @param {number} ping Ping
+ * @param {dayjs.Dayjs} date Date (Only for migration)
+ * @returns {dayjs.Dayjs} date
+ * @throws {Error} Invalid status
+ */
+ async update(status, ping = 0, date) {
+ if (!date) {
+ date = this.getCurrentDate();
+ }
+
+ let flatStatus = this.flatStatus(status);
+
+ if (flatStatus === DOWN && ping > 0) {
+ log.debug("uptime-calc", "The ping is not effective when the status is DOWN");
+ }
+
+ let divisionKey = this.getMinutelyKey(date);
+ let hourlyKey = this.getHourlyKey(date);
+ let dailyKey = this.getDailyKey(date);
+
+ let minutelyData = this.minutelyUptimeDataList[divisionKey];
+ let hourlyData = this.hourlyUptimeDataList[hourlyKey];
+ let dailyData = this.dailyUptimeDataList[dailyKey];
+
+ if (status === MAINTENANCE) {
+ minutelyData.maintenance = minutelyData.maintenance ? minutelyData.maintenance + 1 : 1;
+ hourlyData.maintenance = hourlyData.maintenance ? hourlyData.maintenance + 1 : 1;
+ dailyData.maintenance = dailyData.maintenance ? dailyData.maintenance + 1 : 1;
+
+ } else if (flatStatus === UP) {
+ minutelyData.up += 1;
+ hourlyData.up += 1;
+ dailyData.up += 1;
+
+ // Only UP status can update the ping
+ if (!isNaN(ping)) {
+ // Add avg ping
+ // The first beat of the minute, the ping is the current ping
+ if (minutelyData.up === 1) {
+ minutelyData.avgPing = ping;
+ minutelyData.minPing = ping;
+ minutelyData.maxPing = ping;
+ } else {
+ minutelyData.avgPing = (minutelyData.avgPing * (minutelyData.up - 1) + ping) / minutelyData.up;
+ minutelyData.minPing = Math.min(minutelyData.minPing, ping);
+ minutelyData.maxPing = Math.max(minutelyData.maxPing, ping);
+ }
+
+ // Add avg ping
+ // The first beat of the hour, the ping is the current ping
+ if (hourlyData.up === 1) {
+ hourlyData.avgPing = ping;
+ hourlyData.minPing = ping;
+ hourlyData.maxPing = ping;
+ } else {
+ hourlyData.avgPing = (hourlyData.avgPing * (hourlyData.up - 1) + ping) / hourlyData.up;
+ hourlyData.minPing = Math.min(hourlyData.minPing, ping);
+ hourlyData.maxPing = Math.max(hourlyData.maxPing, ping);
+ }
+
+ // Add avg ping (daily)
+ // The first beat of the day, the ping is the current ping
+ if (dailyData.up === 1) {
+ dailyData.avgPing = ping;
+ dailyData.minPing = ping;
+ dailyData.maxPing = ping;
+ } else {
+ dailyData.avgPing = (dailyData.avgPing * (dailyData.up - 1) + ping) / dailyData.up;
+ dailyData.minPing = Math.min(dailyData.minPing, ping);
+ dailyData.maxPing = Math.max(dailyData.maxPing, ping);
+ }
+ }
+
+ } else if (flatStatus === DOWN) {
+ minutelyData.down += 1;
+ hourlyData.down += 1;
+ dailyData.down += 1;
+ }
+
+ if (minutelyData !== this.lastUptimeData) {
+ this.lastUptimeData = minutelyData;
+ }
+
+ if (hourlyData !== this.lastHourlyUptimeData) {
+ this.lastHourlyUptimeData = hourlyData;
+ }
+
+ if (dailyData !== this.lastDailyUptimeData) {
+ this.lastDailyUptimeData = dailyData;
+ }
+
+ // Don't store data in test mode
+ if (process.env.TEST_BACKEND) {
+ log.debug("uptime-calc", "Skip storing data in test mode");
+ return date;
+ }
+
+ let dailyStatBean = await this.getDailyStatBean(dailyKey);
+ dailyStatBean.up = dailyData.up;
+ dailyStatBean.down = dailyData.down;
+ dailyStatBean.ping = dailyData.avgPing;
+ dailyStatBean.pingMin = dailyData.minPing;
+ dailyStatBean.pingMax = dailyData.maxPing;
+ {
+ // eslint-disable-next-line no-unused-vars
+ const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = dailyData;
+ if (Object.keys(extras).length > 0) {
+ dailyStatBean.extras = JSON.stringify(extras);
+ }
+ }
+ await R.store(dailyStatBean);
+
+ let currentDate = this.getCurrentDate();
+
+ // For migration mode, we don't need to store old hourly and minutely data, but we need 30-day's hourly data
+ // Run anyway for non-migration mode
+ if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statHourlyKeepDay, "day"))) {
+ let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
+ hourlyStatBean.up = hourlyData.up;
+ hourlyStatBean.down = hourlyData.down;
+ hourlyStatBean.ping = hourlyData.avgPing;
+ hourlyStatBean.pingMin = hourlyData.minPing;
+ hourlyStatBean.pingMax = hourlyData.maxPing;
+ {
+ // eslint-disable-next-line no-unused-vars
+ const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
+ if (Object.keys(extras).length > 0) {
+ hourlyStatBean.extras = JSON.stringify(extras);
+ }
+ }
+ await R.store(hourlyStatBean);
+ }
+
+ // For migration mode, we don't need to store old hourly and minutely data, but we need 24-hour's minutely data
+ // Run anyway for non-migration mode
+ if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statMinutelyKeepHour, "hour"))) {
+ let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
+ minutelyStatBean.up = minutelyData.up;
+ minutelyStatBean.down = minutelyData.down;
+ minutelyStatBean.ping = minutelyData.avgPing;
+ minutelyStatBean.pingMin = minutelyData.minPing;
+ minutelyStatBean.pingMax = minutelyData.maxPing;
+ {
+ // eslint-disable-next-line no-unused-vars
+ const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
+ if (Object.keys(extras).length > 0) {
+ minutelyStatBean.extras = JSON.stringify(extras);
+ }
+ }
+ await R.store(minutelyStatBean);
+ }
+
+ // No need to remove old data in migration mode
+ if (!this.migrationMode) {
+ // Remove the old data
+ // TODO: Improvement: Convert it to a job?
+ log.debug("uptime-calc", "Remove old data");
+ await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
+ this.monitorID,
+ this.getMinutelyKey(currentDate.subtract(this.statMinutelyKeepHour, "hour")),
+ ]);
+
+ await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
+ this.monitorID,
+ this.getHourlyKey(currentDate.subtract(this.statHourlyKeepDay, "day")),
+ ]);
+ }
+
+ return date;
+ }
+
+ /**
+ * Get the daily stat bean
+ * @param {number} timestamp milliseconds
+ * @returns {Promise<import("redbean-node").Bean>} stat_daily bean
+ */
+ async getDailyStatBean(timestamp) {
+ if (this.lastDailyStatBean && this.lastDailyStatBean.timestamp === timestamp) {
+ return this.lastDailyStatBean;
+ }
+
+ let bean = await R.findOne("stat_daily", " monitor_id = ? AND timestamp = ?", [
+ this.monitorID,
+ timestamp,
+ ]);
+
+ if (!bean) {
+ bean = R.dispense("stat_daily");
+ bean.monitor_id = this.monitorID;
+ bean.timestamp = timestamp;
+ }
+
+ this.lastDailyStatBean = bean;
+ return this.lastDailyStatBean;
+ }
+
+ /**
+ * Get the hourly stat bean
+ * @param {number} timestamp milliseconds
+ * @returns {Promise<import("redbean-node").Bean>} stat_hourly bean
+ */
+ async getHourlyStatBean(timestamp) {
+ if (this.lastHourlyStatBean && this.lastHourlyStatBean.timestamp === timestamp) {
+ return this.lastHourlyStatBean;
+ }
+
+ let bean = await R.findOne("stat_hourly", " monitor_id = ? AND timestamp = ?", [
+ this.monitorID,
+ timestamp,
+ ]);
+
+ if (!bean) {
+ bean = R.dispense("stat_hourly");
+ bean.monitor_id = this.monitorID;
+ bean.timestamp = timestamp;
+ }
+
+ this.lastHourlyStatBean = bean;
+ return this.lastHourlyStatBean;
+ }
+
+ /**
+ * Get the minutely stat bean
+ * @param {number} timestamp milliseconds
+ * @returns {Promise<import("redbean-node").Bean>} stat_minutely bean
+ */
+ async getMinutelyStatBean(timestamp) {
+ if (this.lastMinutelyStatBean && this.lastMinutelyStatBean.timestamp === timestamp) {
+ return this.lastMinutelyStatBean;
+ }
+
+ let bean = await R.findOne("stat_minutely", " monitor_id = ? AND timestamp = ?", [
+ this.monitorID,
+ timestamp,
+ ]);
+
+ if (!bean) {
+ bean = R.dispense("stat_minutely");
+ bean.monitor_id = this.monitorID;
+ bean.timestamp = timestamp;
+ }
+
+ this.lastMinutelyStatBean = bean;
+ return this.lastMinutelyStatBean;
+ }
+
+ /**
+ * Convert timestamp to minutely key
+ * @param {dayjs.Dayjs} date The heartbeat date
+ * @returns {number} Timestamp
+ */
+ getMinutelyKey(date) {
+ // Truncate value to minutes (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
+ date = date.startOf("minute");
+
+ // Convert to timestamp in second
+ let divisionKey = date.unix();
+
+ if (! (divisionKey in this.minutelyUptimeDataList)) {
+ this.minutelyUptimeDataList.push(divisionKey, {
+ up: 0,
+ down: 0,
+ avgPing: 0,
+ minPing: 0,
+ maxPing: 0,
+ });
+ }
+
+ return divisionKey;
+ }
+
+ /**
+ * Convert timestamp to hourly key
+ * @param {dayjs.Dayjs} date The heartbeat date
+ * @returns {number} Timestamp
+ */
+ getHourlyKey(date) {
+ // Truncate value to hours (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:00:00)
+ date = date.startOf("hour");
+
+ // Convert to timestamp in second
+ let divisionKey = date.unix();
+
+ if (! (divisionKey in this.hourlyUptimeDataList)) {
+ this.hourlyUptimeDataList.push(divisionKey, {
+ up: 0,
+ down: 0,
+ avgPing: 0,
+ minPing: 0,
+ maxPing: 0,
+ });
+ }
+
+ return divisionKey;
+ }
+
+ /**
+ * Convert timestamp to daily key
+ * @param {dayjs.Dayjs} date The heartbeat date
+ * @returns {number} Timestamp
+ */
+ getDailyKey(date) {
+ // Truncate value to start of day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
+ // Considering if the user keep changing could affect the calculation, so use UTC time to avoid this problem.
+ date = date.utc().startOf("day");
+ let dailyKey = date.unix();
+
+ if (!this.dailyUptimeDataList[dailyKey]) {
+ this.dailyUptimeDataList.push(dailyKey, {
+ up: 0,
+ down: 0,
+ avgPing: 0,
+ minPing: 0,
+ maxPing: 0,
+ });
+ }
+
+ return dailyKey;
+ }
+
+ /**
+ * Convert timestamp to key
+ * @param {dayjs.Dayjs} datetime Datetime
+ * @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
+ * @returns {number} Timestamp
+ * @throws {Error} If the type is invalid
+ */
+ getKey(datetime, type) {
+ switch (type) {
+ case "day":
+ return this.getDailyKey(datetime);
+ case "hour":
+ return this.getHourlyKey(datetime);
+ case "minute":
+ return this.getMinutelyKey(datetime);
+ default:
+ throw new Error("Invalid type");
+ }
+ }
+
+ /**
+ * Flat status to UP or DOWN
+ * @param {number} status the status which should be turned into a flat status
+ * @returns {UP|DOWN|PENDING} The flat status
+ * @throws {Error} Invalid status
+ */
+ flatStatus(status) {
+ switch (status) {
+ case UP:
+ case MAINTENANCE:
+ return UP;
+ case DOWN:
+ case PENDING:
+ return DOWN;
+ }
+ throw new Error("Invalid status");
+ }
+
+ /**
+ * @param {number} num the number of data points which are expected to be returned
+ * @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
+ * @returns {UptimeDataResult} UptimeDataResult
+ * @throws {Error} The maximum number of minutes greater than 1440
+ */
+ getData(num, type = "day") {
+
+ if (type === "hour" && num > 24 * 30) {
+ throw new Error("The maximum number of hours is 720");
+ }
+ if (type === "minute" && num > 24 * 60) {
+ throw new Error("The maximum number of minutes is 1440");
+ }
+ if (type === "day" && num > 365) {
+ throw new Error("The maximum number of days is 365");
+ }
+ // Get the current time period key based on the type
+ let key = this.getKey(this.getCurrentDate(), type);
+
+ let total = {
+ up: 0,
+ down: 0,
+ };
+
+ let totalPing = 0;
+ let endTimestamp;
+
+ // Get the eariest timestamp of the required period based on the type
+ switch (type) {
+ case "day":
+ endTimestamp = key - 86400 * (num - 1);
+ break;
+ case "hour":
+ endTimestamp = key - 3600 * (num - 1);
+ break;
+ case "minute":
+ endTimestamp = key - 60 * (num - 1);
+ break;
+ default:
+ throw new Error("Invalid type");
+ }
+
+ // Sum up all data in the specified time range
+ while (key >= endTimestamp) {
+ let data;
+
+ switch (type) {
+ case "day":
+ data = this.dailyUptimeDataList[key];
+ break;
+ case "hour":
+ data = this.hourlyUptimeDataList[key];
+ break;
+ case "minute":
+ data = this.minutelyUptimeDataList[key];
+ break;
+ default:
+ throw new Error("Invalid type");
+ }
+
+ if (data) {
+ total.up += data.up;
+ total.down += data.down;
+ totalPing += data.avgPing * data.up;
+ }
+
+ // Set key to the previous time period
+ switch (type) {
+ case "day":
+ key -= 86400;
+ break;
+ case "hour":
+ key -= 3600;
+ break;
+ case "minute":
+ key -= 60;
+ break;
+ default:
+ throw new Error("Invalid type");
+ }
+ }
+
+ let uptimeData = new UptimeDataResult();
+
+ // If there is no data in the previous time ranges, use the last data?
+ if (total.up === 0 && total.down === 0) {
+ switch (type) {
+ case "day":
+ if (this.lastDailyUptimeData) {
+ total = this.lastDailyUptimeData;
+ totalPing = total.avgPing * total.up;
+ } else {
+ return uptimeData;
+ }
+ break;
+ case "hour":
+ if (this.lastHourlyUptimeData) {
+ total = this.lastHourlyUptimeData;
+ totalPing = total.avgPing * total.up;
+ } else {
+ return uptimeData;
+ }
+ break;
+ case "minute":
+ if (this.lastUptimeData) {
+ total = this.lastUptimeData;
+ totalPing = total.avgPing * total.up;
+ } else {
+ return uptimeData;
+ }
+ break;
+ default:
+ throw new Error("Invalid type");
+ }
+ }
+
+ let avgPing;
+
+ if (total.up === 0) {
+ avgPing = null;
+ } else {
+ avgPing = totalPing / total.up;
+ }
+
+ if (total.up + total.down === 0) {
+ uptimeData.uptime = 0;
+ } else {
+ uptimeData.uptime = total.up / (total.up + total.down);
+ }
+ uptimeData.avgPing = avgPing;
+ return uptimeData;
+ }
+
+ /**
+ * Get data in form of an array
+ * @param {number} num the number of data points which are expected to be returned
+ * @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
+ * @returns {Array<object>} uptime data
+ * @throws {Error} The maximum number of minutes greater than 1440
+ */
+ getDataArray(num, type = "day") {
+ if (type === "hour" && num > 24 * 30) {
+ throw new Error("The maximum number of hours is 720");
+ }
+ if (type === "minute" && num > 24 * 60) {
+ throw new Error("The maximum number of minutes is 1440");
+ }
+
+ // Get the current time period key based on the type
+ let key = this.getKey(this.getCurrentDate(), type);
+
+ let result = [];
+
+ let endTimestamp;
+
+ // Get the eariest timestamp of the required period based on the type
+ switch (type) {
+ case "day":
+ endTimestamp = key - 86400 * (num - 1);
+ break;
+ case "hour":
+ endTimestamp = key - 3600 * (num - 1);
+ break;
+ case "minute":
+ endTimestamp = key - 60 * (num - 1);
+ break;
+ default:
+ throw new Error("Invalid type");
+ }
+
+ // Get datapoints in the specified time range
+ while (key >= endTimestamp) {
+ let data;
+
+ switch (type) {
+ case "day":
+ data = this.dailyUptimeDataList[key];
+ break;
+ case "hour":
+ data = this.hourlyUptimeDataList[key];
+ break;
+ case "minute":
+ data = this.minutelyUptimeDataList[key];
+ break;
+ default:
+ throw new Error("Invalid type");
+ }
+
+ if (data) {
+ data.timestamp = key;
+ result.push(data);
+ }
+
+ // Set key to the previous time period
+ switch (type) {
+ case "day":
+ key -= 86400;
+ break;
+ case "hour":
+ key -= 3600;
+ break;
+ case "minute":
+ key -= 60;
+ break;
+ default:
+ throw new Error("Invalid type");
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Get the uptime data for given duration.
+ * @param {string} duration A string with a number and a unit (m,h,d,w,M,y), such as 24h, 30d, 1y.
+ * @returns {UptimeDataResult} UptimeDataResult
+ * @throws {Error} Invalid duration / Unsupported unit
+ */
+ getDataByDuration(duration) {
+ const durationNumStr = duration.slice(0, -1);
+
+ if (!/^[0-9]+$/.test(durationNumStr)) {
+ throw new Error(`Invalid duration: ${duration}`);
+ }
+ const num = Number(durationNumStr);
+ const unit = duration.slice(-1);
+
+ switch (unit) {
+ case "m":
+ return this.getData(num, "minute");
+ case "h":
+ return this.getData(num, "hour");
+ case "d":
+ return this.getData(num, "day");
+ case "w":
+ return this.getData(7 * num, "day");
+ case "M":
+ return this.getData(30 * num, "day");
+ case "y":
+ return this.getData(365 * num, "day");
+ default:
+ throw new Error(`Unsupported unit (${unit}) for badge duration ${duration}`
+ );
+ }
+ }
+
+ /**
+ * 1440 = 24 * 60mins
+ * @returns {UptimeDataResult} UptimeDataResult
+ */
+ get24Hour() {
+ return this.getData(1440, "minute");
+ }
+
+ /**
+ * @returns {UptimeDataResult} UptimeDataResult
+ */
+ get7Day() {
+ return this.getData(168, "hour");
+ }
+
+ /**
+ * @returns {UptimeDataResult} UptimeDataResult
+ */
+ get30Day() {
+ return this.getData(30);
+ }
+
+ /**
+ * @returns {UptimeDataResult} UptimeDataResult
+ */
+ get1Year() {
+ return this.getData(365);
+ }
+
+ /**
+ * @returns {dayjs.Dayjs} Current datetime in UTC
+ */
+ getCurrentDate() {
+ return dayjs.utc();
+ }
+
+ /**
+ * For migration purposes.
+ * @param {boolean} value Migration mode on/off
+ * @returns {void}
+ */
+ setMigrationMode(value) {
+ this.migrationMode = value;
+ }
+}
+
+class UptimeDataResult {
+ /**
+ * @type {number} Uptime
+ */
+ uptime = 0;
+
+ /**
+ * @type {number} Average ping
+ */
+ avgPing = null;
+}
+
+module.exports = {
+ UptimeCalculator,
+ UptimeDataResult,
+};