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",