From 6caf37a0032c384f4c142674da0ffe8908ce894a Mon Sep 17 00:00:00 2001 From: Ricardo Date: Sat, 7 Feb 2026 00:52:29 +0100 Subject: [PATCH] feat: add startup cleanup for old read items Runs cleanupAllReadItems on server startup to clean up accumulated read items from all channels, keeping only the 30 most recent per channel per user. Co-Authored-By: Claude Opus 4.5 --- index.js | 7 ++++- lib/storage/items.js | 62 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 068126b..6bf783e 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ import { microsubController } from "./lib/controllers/microsub.js"; import { readerController } from "./lib/controllers/reader.js"; import { handleMediaProxy } from "./lib/media/proxy.js"; import { startScheduler, stopScheduler } from "./lib/polling/scheduler.js"; -import { createIndexes } from "./lib/storage/items.js"; +import { cleanupAllReadItems, createIndexes } from "./lib/storage/items.js"; import { webmentionReceiver } from "./lib/webmention/receiver.js"; import { websubHandler } from "./lib/websub/handler.js"; @@ -157,6 +157,11 @@ export default class MicrosubEndpoint { createIndexes(indiekit).catch((error) => { console.warn("[Microsub] Index creation failed:", error.message); }); + + // Cleanup old read items on startup + cleanupAllReadItems(indiekit).catch((error) => { + console.warn("[Microsub] Startup cleanup failed:", error.message); + }); } else { console.warn( "[Microsub] Database not available at init, scheduler not started", diff --git a/lib/storage/items.js b/lib/storage/items.js index 002762b..ed6d7ab 100644 --- a/lib/storage/items.js +++ b/lib/storage/items.js @@ -328,6 +328,68 @@ async function cleanupOldReadItems(collection, channelObjectId, userId) { } } +/** + * Cleanup all read items across all channels (startup cleanup) + * @param {object} application - Indiekit application + * @returns {Promise} Total number of items deleted + */ +export async function cleanupAllReadItems(application) { + const collection = getCollection(application); + const channelsCollection = application.collections.get("microsub_channels"); + + // Get all channels + const channels = await channelsCollection.find({}).toArray(); + let totalDeleted = 0; + + for (const channel of channels) { + // Get unique userIds who have read items in this channel + const readByUsers = await collection.distinct("readBy", { + channelId: channel._id, + readBy: { $exists: true, $ne: [] }, + }); + + for (const userId of readByUsers) { + if (!userId) continue; + + const readCount = await collection.countDocuments({ + channelId: channel._id, + readBy: userId, + }); + + if (readCount > MAX_READ_ITEMS) { + const itemsToDelete = await collection + .find({ + channelId: channel._id, + readBy: userId, + }) + .sort({ published: -1, _id: -1 }) + .skip(MAX_READ_ITEMS) + .project({ _id: 1 }) + .toArray(); + + if (itemsToDelete.length > 0) { + const idsToDelete = itemsToDelete.map((item) => item._id); + const deleteResult = await collection.deleteMany({ + _id: { $in: idsToDelete }, + }); + totalDeleted += deleteResult.deletedCount; + console.info( + `[Microsub] Startup cleanup: deleted ${deleteResult.deletedCount} old items from channel "${channel.name}"`, + ); + } + } + } + } + + if (totalDeleted > 0) { + console.info( + `[Microsub] Startup cleanup complete: ${totalDeleted} total items deleted`, + ); + } + + return totalDeleted; +} + export async function markItemsRead(application, channelId, entryIds, userId) { const collection = getCollection(application); const channelObjectId = diff --git a/package.json b/package.json index d5857f2..fa5ceb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-microsub", - "version": "1.0.21", + "version": "1.0.22", "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.", "keywords": [ "indiekit",