Files
homefeed/backend/src/queue/retention.ts
T
2026-07-21 12:51:31 -04:00

65 lines
2.9 KiB
TypeScript

import * as articlesDb from '../storage/db/articles.js';
import * as contentItemsDb from '../storage/db/contentItems.js';
import * as tagsDb from '../storage/db/tags.js';
import * as eventsDb from '../storage/db/events.js';
import { deleteCandidateMediaOlderThan, totalStorageBytes } from '../storage/media/index.js';
import { logger } from '../storage/db/logs.js';
import type { GlobalSettings } from '../storage/db/types.js';
/**
* One sweep, evaluating both dimensions from the schema doc: age-based limits and the
* size-based FIFO cap. Whichever fires, oldest-first. Tracked events can override the
* published-article age limit (e.g. "Iran war" recaps kept forever) — see EventsTab.
*/
export function runRetentionSweep(settings: GlobalSettings) {
const { retention } = settings;
if (retention.rawItemMaxAgeDays != null) {
const stale = contentItemsDb.itemsOlderThan(retention.rawItemMaxAgeDays);
if (stale.length > 0) {
contentItemsDb.deleteContentItems(stale.map((i) => i.id));
logger.info('retention', `Pruned ${stale.length} raw item(s) older than ${retention.rawItemMaxAgeDays}d`);
}
}
if (retention.publishedArticleMaxAgeDays != null) {
const eventOverrides = new Map(eventsDb.listEvents().map((e) => [e.id, e.retentionOverrideDays]));
const stale = articlesDb.articlesOlderThan(retention.publishedArticleMaxAgeDays);
for (const article of stale) {
const override = article.eventId ? eventOverrides.get(article.eventId) : undefined;
if (override === null) continue; // explicit "Forever" override
if (override !== undefined) {
const overrideCutoff = Date.now() - override * 86_400_000;
if (new Date(article.publishedAt).getTime() > overrideCutoff) continue;
}
articlesDb.deleteArticle(article.id);
}
}
deleteCandidateMediaOlderThan(retention.rawItemMaxAgeDays ?? 7);
if (retention.storageCapEnabled) {
enforceStorageCap(retention.storageCapValue, retention.storageCapUnit);
}
const expiredTags = tagsDb.expireStaleTags(settings.tagExpiryDays);
if (expiredTags > 0) logger.info('retention', `Expired ${expiredTags} stale tag(s)`);
}
function enforceStorageCap(capValue: number, unit: 'MB' | 'GB') {
const capBytes = capValue * (unit === 'GB' ? 1024 * 1024 * 1024 : 1024 * 1024);
let used = totalStorageBytes();
if (used <= capBytes) return;
// FIFO: delete oldest published articles (and their media, via cascade at the DB
// level being absent here — media rows are cleaned up by the candidate sweep above;
// published-tier media tied to a deleted article becomes orphaned and is swept next
// cycle once its downloaded_at also ages past raw-item retention as a backstop).
const oldest = articlesDb.allArticlesNewestFirst().reverse(); // oldest first — retention needs everything, not the feed's page size
for (const article of oldest) {
if (used <= capBytes) break;
articlesDb.deleteArticle(article.id);
used = totalStorageBytes();
}
}