@rmdes/indiekit-endpoint-microsub 1.0.42 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -9,7 +9,11 @@ import { readerController } from "./lib/controllers/reader.js";
9
9
  import { handleMediaProxy } from "./lib/media/proxy.js";
10
10
  import { startScheduler, stopScheduler } from "./lib/polling/scheduler.js";
11
11
  import { ensureActivityPubChannel } from "./lib/storage/channels.js";
12
- import { cleanupAllReadItems, createIndexes } from "./lib/storage/items.js";
12
+ import {
13
+ cleanupAllReadItems,
14
+ cleanupStaleItems,
15
+ createIndexes,
16
+ } from "./lib/storage/items.js";
13
17
  import { webmentionReceiver } from "./lib/webmention/receiver.js";
14
18
  import { websubHandler } from "./lib/websub/handler.js";
15
19
 
@@ -210,6 +214,11 @@ export default class MicrosubEndpoint {
210
214
  cleanupAllReadItems(indiekit).catch((error) => {
211
215
  console.warn("[Microsub] Startup cleanup failed:", error.message);
212
216
  });
217
+
218
+ // Delete stale items (stripped skeletons + unread older than 30 days)
219
+ cleanupStaleItems(indiekit).catch((error) => {
220
+ console.warn("[Microsub] Stale cleanup failed:", error.message);
221
+ });
213
222
  } else {
214
223
  console.warn(
215
224
  "[Microsub] Database not available at init, scheduler not started",
@@ -387,6 +387,10 @@ export async function countReadItems(application, channelId, userId) {
387
387
  // uid, readBy) so the poller doesn't re-ingest them as new unread entries.
388
388
  const MAX_FULL_READ_ITEMS = 200;
389
389
 
390
+ // Maximum age (in days) for stripped skeletons and unread items.
391
+ // After this period, both are hard-deleted to prevent unbounded growth.
392
+ const MAX_ITEM_AGE_DAYS = 30;
393
+
390
394
  /**
391
395
  * Cleanup old read items by stripping content but preserving dedup skeletons.
392
396
  * This prevents the vicious cycle where deleted read items get re-ingested as
@@ -575,6 +579,63 @@ export async function cleanupAllReadItems(application) {
575
579
  return totalCleaned;
576
580
  }
577
581
 
582
+ /**
583
+ * Delete stale items: stripped skeletons and unread items older than MAX_ITEM_AGE_DAYS.
584
+ * Stripped skeletons have served their dedup purpose; stale unread items are unlikely
585
+ * to be read. Both are hard-deleted to prevent unbounded collection growth.
586
+ * @param {object} application - Indiekit application
587
+ * @returns {Promise<number>} Total number of items deleted
588
+ */
589
+ export async function cleanupStaleItems(application) {
590
+ const collection = getCollection(application);
591
+ const cutoff = new Date();
592
+ cutoff.setDate(cutoff.getDate() - MAX_ITEM_AGE_DAYS);
593
+
594
+ // Delete stripped skeletons older than cutoff
595
+ const strippedResult = await collection.deleteMany({
596
+ _stripped: true,
597
+ $or: [
598
+ { published: { $lt: cutoff } },
599
+ { published: { $exists: false }, createdAt: { $lt: cutoff.toISOString() } },
600
+ ],
601
+ });
602
+
603
+ // Delete unread items older than cutoff
604
+ const unreadResult = await collection.deleteMany({
605
+ readBy: { $in: [null, []] },
606
+ _stripped: { $ne: true },
607
+ $or: [
608
+ { published: { $lt: cutoff } },
609
+ { published: { $exists: false }, createdAt: { $lt: cutoff.toISOString() } },
610
+ ],
611
+ });
612
+
613
+ // Also catch items with no readBy field at all
614
+ const noReadByResult = await collection.deleteMany({
615
+ readBy: { $exists: false },
616
+ _stripped: { $ne: true },
617
+ $or: [
618
+ { published: { $lt: cutoff } },
619
+ { published: { $exists: false }, createdAt: { $lt: cutoff.toISOString() } },
620
+ ],
621
+ });
622
+
623
+ const total =
624
+ strippedResult.deletedCount +
625
+ unreadResult.deletedCount +
626
+ noReadByResult.deletedCount;
627
+
628
+ if (total > 0) {
629
+ console.info(
630
+ `[Microsub] Stale cleanup: deleted ${strippedResult.deletedCount} stripped skeletons, ` +
631
+ `${unreadResult.deletedCount + noReadByResult.deletedCount} stale unread items ` +
632
+ `(cutoff: ${MAX_ITEM_AGE_DAYS} days)`,
633
+ );
634
+ }
635
+
636
+ return total;
637
+ }
638
+
578
639
  export async function markItemsRead(application, channelId, entryIds, userId) {
579
640
  const collection = getCollection(application);
580
641
  const channelObjectId =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",