48hr.email/domain/statistics-store.js
2026-01-03 17:00:39 +01:00

199 lines
5.5 KiB
JavaScript

const debug = require('debug')('48hr-email:stats-store')
/**
* Statistics Store - Tracks email metrics and historical data
* Stores 24-hour rolling statistics for receives, deletes, and forwards
*/
class StatisticsStore {
constructor() {
// Current totals
this.currentCount = 0
this.largestUid = 0
// 24-hour rolling data (one entry per minute = 1440 entries)
this.hourlyData = []
this.maxDataPoints = 24 * 60 // 24 hours * 60 minutes
// Track last cleanup to avoid too frequent operations
this.lastCleanup = Date.now()
debug('Statistics store initialized')
}
/**
* Initialize with current email count
* @param {number} count - Current email count
*/
initialize(count) {
this.currentCount = count
debug(`Initialized with ${count} emails`)
}
/**
* Update largest UID (all-time total emails processed)
* @param {number} uid - Largest UID from mailbox (0 if no emails)
*/
updateLargestUid(uid) {
if (uid >= 0 && uid > this.largestUid) {
this.largestUid = uid
debug(`Largest UID updated to ${uid}`)
}
}
/**
* Record an email received event
*/
recordReceive() {
this.currentCount++
this._addDataPoint('receive')
debug(`Email received. Current: ${this.currentCount}`)
}
/**
* Record an email deleted event
*/
recordDelete() {
this.currentCount = Math.max(0, this.currentCount - 1)
this._addDataPoint('delete')
debug(`Email deleted. Current: ${this.currentCount}`)
}
/**
* Record an email forwarded event
*/
recordForward() {
this._addDataPoint('forward')
debug(`Email forwarded`)
}
/**
* Update current count (for bulk operations like purge)
* @param {number} count - New current count
*/
updateCurrentCount(count) {
const diff = count - this.currentCount
if (diff < 0) {
// Bulk delete occurred
for (let i = 0; i < Math.abs(diff); i++) {
this._addDataPoint('delete')
}
}
this.currentCount = count
debug(`Current count updated to ${count}`)
}
/**
* Get current statistics
* @returns {Object} Current stats
*/
getStats() {
this._cleanup()
const last24h = this._getLast24Hours()
return {
currentCount: this.currentCount,
allTimeTotal: this.largestUid,
last24Hours: {
receives: last24h.receives,
deletes: last24h.deletes,
forwards: last24h.forwards,
timeline: this._getTimeline()
}
}
}
/**
* Add a data point to the rolling history
* @param {string} type - Type of event (receive, delete, forward)
* @private
*/
_addDataPoint(type) {
const now = Date.now()
const minute = Math.floor(now / 60000) * 60000 // Round to minute
// Find or create entry for this minute
let entry = this.hourlyData.find(e => e.timestamp === minute)
if (!entry) {
entry = {
timestamp: minute,
receives: 0,
deletes: 0,
forwards: 0
}
this.hourlyData.push(entry)
}
entry[type + 's']++
this._cleanup()
}
/**
* Clean up old data points (older than 24 hours)
* @private
*/
_cleanup() {
const now = Date.now()
// Only cleanup every 5 minutes to avoid constant filtering
if (now - this.lastCleanup < 5 * 60 * 1000) {
return
}
const cutoff = now - (24 * 60 * 60 * 1000)
const beforeCount = this.hourlyData.length
this.hourlyData = this.hourlyData.filter(entry => entry.timestamp >= cutoff)
if (beforeCount !== this.hourlyData.length) {
debug(`Cleaned up ${beforeCount - this.hourlyData.length} old data points`)
}
this.lastCleanup = now
}
/**
* Get aggregated stats for last 24 hours
* @returns {Object} Aggregated counts
* @private
*/
_getLast24Hours() {
const cutoff = Date.now() - (24 * 60 * 60 * 1000)
const recent = this.hourlyData.filter(e => e.timestamp >= cutoff)
return {
receives: recent.reduce((sum, e) => sum + e.receives, 0),
deletes: recent.reduce((sum, e) => sum + e.deletes, 0),
forwards: recent.reduce((sum, e) => sum + e.forwards, 0)
}
}
/**
* Get timeline data for graphing (hourly aggregates)
* @returns {Array} Array of hourly data points
* @private
*/
_getTimeline() {
const now = Date.now()
const cutoff = now - (24 * 60 * 60 * 1000)
const hourly = {}
// Aggregate by hour
this.hourlyData
.filter(e => e.timestamp >= cutoff)
.forEach(entry => {
const hour = Math.floor(entry.timestamp / 3600000) * 3600000
if (!hourly[hour]) {
hourly[hour] = { timestamp: hour, receives: 0, deletes: 0, forwards: 0 }
}
hourly[hour].receives += entry.receives
hourly[hour].deletes += entry.deletes
hourly[hour].forwards += entry.forwards
})
// Convert to sorted array
return Object.values(hourly).sort((a, b) => a.timestamp - b.timestamp)
}
}
module.exports = StatisticsStore