Usage — total data dir, Corestore, memory rings, warm HyperDB sample
CI / test (push) Successful in 1m2s
Release rolling / release (push) Has been cancelled

Hot memory — 15m → 24h (or custom points)
Warm memory — downsampled ring + samples-per-bucket
Disk history — auto-rotate by age (1 day → 1 year, forever, or custom days)
Soft disk budget + max warm points
Auto prune on a configurable hour interval
Dry-run prune / Prune now / Save to agent (admin role)
This commit is contained in:
Raven Scott
2026-07-19 12:55:13 -04:00
parent b5b2907496
commit c3b4240e47
26 changed files with 1610 additions and 17 deletions
+13
View File
@@ -65,3 +65,16 @@ export function formatKilobitsPerSec(kbps) {
const { n, unit } = scaleUnit(bps, ['b/s', 'kb/s', 'Mb/s', 'Gb/s', 'Tb/s'], 1000)
return `${trimFixed(n, autoDigits(n))} ${unit}`
}
/**
* Format raw bytes → B / KiB / MiB / GiB / TiB / PiB.
* @param {number|null|undefined} bytes
* @returns {string}
*/
export function formatBytes(bytes) {
if (bytes == null || !Number.isFinite(Number(bytes))) return '—'
const n0 = Number(bytes)
if (n0 === 0) return '0 B'
const { n, unit } = scaleUnit(n0, ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'], 1024)
return `${trimFixed(n, autoDigits(n))} ${unit}`
}
+6
View File
@@ -91,6 +91,12 @@ export const MethodRoles = Object.freeze({
linkPeer: Roles.admin,
unlinkPeer: Roles.admin,
// Data Manager — retention + storage
getStorageInfo: Roles.viewer,
getRetentionConfig: Roles.viewer,
setRetentionConfig: Roles.admin,
pruneHistory: Roles.admin,
// parent / fleet aggregation
getFleetHealth: Roles.viewer,
listChildPeers: Roles.viewer,
+180
View File
@@ -0,0 +1,180 @@
/**
* 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,
warmRetentionPreset: '1w',
warmRetentionMs: RETENTION_PRESETS['1w'].ms,
warmMaxBytes: 0,
warmMaxPoints: 0,
autoPruneEnabled: true,
autoPruneIntervalMs: 60 * 60 * 1000,
lastPruneAt: null,
lastPruneDeleted: 0,
lastPruneBytesFreed: 0,
}
}
/**
* @param {Partial<RetentionConfig>|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<string, { points: number|null }>} 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))
}
+19
View File
@@ -164,8 +164,27 @@ export function validateMethodArgs(method, args = {}) {
case 'listPeerLinks':
case 'getFleetHealth':
case 'listChildPeers':
case 'getStorageInfo':
case 'getRetentionConfig':
return { ok: true, args }
case 'setRetentionConfig': {
const patch = args && typeof args === 'object' ? { ...args } : {}
delete patch.success
return { ok: true, args: patch }
}
case 'pruneHistory': {
return {
ok: true,
args: {
dryRun: Boolean(args?.dryRun),
beforeMs: args?.beforeMs != null ? Number(args.beforeMs) : undefined,
force: Boolean(args?.force),
},
}
}
case 'linkPeer': {
const remotePublicKey = String(args.remotePublicKey || args.peerId || '').toLowerCase()
if (!/^[0-9a-f]{64}$/.test(remotePublicKey)) {