/** * Retention presets + config normalization (client + agent). */ /** @typedef {{ * tier0Points: number, * tier1Points: number, * tier1Every: number, * warmRetentionEnabled: boolean, * warmRetentionPreset: string, * warmRetentionMs: number, * warmMaxBytes: number, * warmMaxPoints: number, * autoPruneEnabled: boolean, * autoPruneIntervalMs: number, * lastPruneAt: number|null, * lastPruneDeleted: number, * lastPruneBytesFreed: number, * }} RetentionConfig */ export const RETENTION_PRESETS = Object.freeze({ '1d': { id: '1d', label: '1 day', ms: 1 * 24 * 60 * 60 * 1000 }, '1w': { id: '1w', label: '1 week', ms: 7 * 24 * 60 * 60 * 1000 }, '2w': { id: '2w', label: '2 weeks', ms: 14 * 24 * 60 * 60 * 1000 }, '1m': { id: '1m', label: '1 month', ms: 30 * 24 * 60 * 60 * 1000 }, '3m': { id: '3m', label: '3 months', ms: 90 * 24 * 60 * 60 * 1000 }, '6m': { id: '6m', label: '6 months', ms: 180 * 24 * 60 * 60 * 1000 }, '1y': { id: '1y', label: '1 year', ms: 365 * 24 * 60 * 60 * 1000 }, forever: { id: 'forever', label: 'Forever', ms: 0 }, custom: { id: 'custom', label: 'Custom', ms: null }, }) /** Hot (1s) ring size presets → points. */ export const HOT_PRESETS = Object.freeze({ '15m': { id: '15m', label: '15 minutes', points: 900 }, '1h': { id: '1h', label: '1 hour', points: 3600 }, '6h': { id: '6h', label: '6 hours', points: 21600 }, '12h': { id: '12h', label: '12 hours', points: 43200 }, '24h': { id: '24h', label: '24 hours', points: 86400 }, custom: { id: 'custom', label: 'Custom', points: null }, }) /** In-memory warm ring presets (@ ~1m resolution). */ export const WARM_MEM_PRESETS = Object.freeze({ '1h': { id: '1h', label: '1 hour', points: 60 }, '6h': { id: '6h', label: '6 hours', points: 360 }, '24h': { id: '24h', label: '24 hours', points: 1440 }, '7d': { id: '7d', label: '7 days', points: 10080 }, custom: { id: 'custom', label: 'Custom', points: null }, }) /** Soft disk budget presets (bytes). 0 = unlimited. */ export const DISK_BUDGET_PRESETS = Object.freeze({ unlimited: { id: 'unlimited', label: 'Unlimited', bytes: 0 }, '256mb': { id: '256mb', label: '256 MB', bytes: 256 * 1024 * 1024 }, '512mb': { id: '512mb', label: '512 MB', bytes: 512 * 1024 * 1024 }, '1gb': { id: '1gb', label: '1 GB', bytes: 1024 * 1024 * 1024 }, '5gb': { id: '5gb', label: '5 GB', bytes: 5 * 1024 * 1024 * 1024 }, '20gb': { id: '20gb', label: '20 GB', bytes: 20 * 1024 * 1024 * 1024 }, custom: { id: 'custom', label: 'Custom', bytes: null }, }) /** * @returns {RetentionConfig} */ export function defaultRetentionConfig() { return { tier0Points: 3600, tier1Points: 1440, tier1Every: 60, warmRetentionEnabled: true, // 3 months of warm history is a better default than 1y for multi-chart hosts warmRetentionPreset: '3m', warmRetentionMs: RETENTION_PRESETS['3m'].ms, // Soft disk budget (0 = unlimited). Default 1 GiB keeps corestore from ballooning. warmMaxBytes: DISK_BUDGET_PRESETS['1gb'].bytes, warmMaxPoints: 0, autoPruneEnabled: true, autoPruneIntervalMs: 60 * 60 * 1000, lastPruneAt: null, lastPruneDeleted: 0, lastPruneBytesFreed: 0, } } /** * @param {Partial|null|undefined} raw * @returns {RetentionConfig} */ export function normalizeRetentionConfig(raw = {}) { const d = defaultRetentionConfig() const src = raw && typeof raw === 'object' ? raw : {} const tier0Points = clampInt(src.tier0Points ?? d.tier0Points, 60, 604_800, d.tier0Points) const tier1Points = clampInt(src.tier1Points ?? d.tier1Points, 10, 525_600, d.tier1Points) const tier1Every = clampInt(src.tier1Every ?? d.tier1Every, 5, 3600, d.tier1Every) let warmRetentionPreset = String(src.warmRetentionPreset || d.warmRetentionPreset) if (!RETENTION_PRESETS[warmRetentionPreset]) warmRetentionPreset = 'custom' let warmRetentionMs = Number(src.warmRetentionMs) if (!Number.isFinite(warmRetentionMs) || warmRetentionMs < 0) warmRetentionMs = d.warmRetentionMs if (warmRetentionPreset !== 'custom' && warmRetentionPreset !== 'forever') { warmRetentionMs = RETENTION_PRESETS[warmRetentionPreset].ms } else if (warmRetentionPreset === 'forever') { warmRetentionMs = 0 } const warmRetentionEnabled = src.warmRetentionEnabled == null ? d.warmRetentionEnabled : Boolean(src.warmRetentionEnabled) const warmMaxBytes = clampInt(src.warmMaxBytes ?? d.warmMaxBytes, 0, Number.MAX_SAFE_INTEGER, 0) const warmMaxPoints = clampInt(src.warmMaxPoints ?? d.warmMaxPoints, 0, Number.MAX_SAFE_INTEGER, 0) const autoPruneEnabled = src.autoPruneEnabled == null ? d.autoPruneEnabled : Boolean(src.autoPruneEnabled) const autoPruneIntervalMs = clampInt( src.autoPruneIntervalMs ?? d.autoPruneIntervalMs, 60_000, 7 * 24 * 60 * 60 * 1000, d.autoPruneIntervalMs ) return { tier0Points, tier1Points, tier1Every, warmRetentionEnabled, warmRetentionPreset, warmRetentionMs, warmMaxBytes, warmMaxPoints, autoPruneEnabled, autoPruneIntervalMs, lastPruneAt: src.lastPruneAt != null && Number.isFinite(Number(src.lastPruneAt)) ? Number(src.lastPruneAt) : null, lastPruneDeleted: clampInt(src.lastPruneDeleted ?? 0, 0, Number.MAX_SAFE_INTEGER, 0), lastPruneBytesFreed: clampInt(src.lastPruneBytesFreed ?? 0, 0, Number.MAX_SAFE_INTEGER, 0), } } /** * Resolve cutoff timestamp for age-based prune (null = skip age prune). * @param {RetentionConfig} cfg * @param {number} [now] */ export function retentionCutoffMs(cfg, now = Date.now()) { if (!cfg?.warmRetentionEnabled) return null if (!cfg.warmRetentionMs || cfg.warmRetentionMs <= 0) return null return now - cfg.warmRetentionMs } /** * @param {number} points * @param {Record} presets */ export function matchPointsPreset(points, presets) { for (const [id, p] of Object.entries(presets)) { if (id === 'custom') continue if (p.points === points) return id } return 'custom' } /** * @param {number} bytes */ export function matchBytesPreset(bytes) { for (const [id, p] of Object.entries(DISK_BUDGET_PRESETS)) { if (id === 'custom') continue if (p.bytes === bytes) return id } return 'custom' } function clampInt(v, min, max, fallback) { const n = Math.floor(Number(v)) if (!Number.isFinite(n)) return fallback return Math.min(max, Math.max(min, n)) }