Files
indiekit-endpoint-activitypub/lib/migrations/separate-mentions.js
Ricardo abf1b94bd6 feat: migrate Fedify KV store and plugin cache from MongoDB to Redis
Replace unbounded ap_kv MongoDB collection (169K docs, 49MB) with Redis:
- Fedify KV store uses @fedify/redis RedisKvStore (native TTL support)
- Plugin cache (fedidb, batch-refollow state, migration flags) uses new
  redis-cache.js utility with indiekit: key prefix
- All controllers updated to remove kvCollection parameter passing
- Addresses OOM kills caused by ap_kv growing ~14K entries/day
2026-03-01 16:26:17 +01:00

83 lines
2.5 KiB
JavaScript

/**
* Migration: separate-mentions
*
* Moves @-prefixed entries from category[] to a new mentions[] array in all
* ap_timeline documents. Tracked in ap_kv for idempotency.
*
* Before: category: ["@user@instance", "hashtag", "@another@host"]
* After: category: ["hashtag"]
* mentions: [{ name: "user@instance", url: "" }, { name: "another@host", url: "" }]
*
* Note: URLs are empty for legacy items since we can't reconstruct them.
* New items will have URLs populated by the fixed extractObjectData() (Task 1).
*/
import { cacheGet, cacheSet } from "../redis-cache.js";
const MIGRATION_KEY = "migration:separate-mentions";
/**
* Run the separate-mentions migration (idempotent)
* @param {object} collections - MongoDB collections
* @returns {Promise<{ skipped: boolean, updated: number }>}
*/
export async function runSeparateMentionsMigration(collections) {
const { ap_timeline } = collections;
// Check if already completed
const state = await cacheGet(MIGRATION_KEY);
if (state?.completed) {
return { skipped: true, updated: 0 };
}
// Find all documents where category[] contains @-prefixed entries
const docs = await ap_timeline
.find({ category: { $regex: /^@/ } })
.toArray();
if (docs.length === 0) {
// No docs to migrate — mark complete immediately
await cacheSet(MIGRATION_KEY, { completed: true, date: new Date().toISOString(), updated: 0 });
return { skipped: false, updated: 0 };
}
// Build bulk operations
const ops = docs.map((doc) => {
const mentions = (doc.mentions || []).slice(); // preserve any existing mentions
const newCategory = [];
for (const entry of doc.category || []) {
if (typeof entry === "string" && entry.startsWith("@")) {
// Move to mentions[] — strip leading @ to match timeline-store convention
const strippedName = entry.slice(1);
const alreadyPresent = mentions.some((m) => m.name === strippedName);
if (!alreadyPresent) {
mentions.push({ name: strippedName, url: "" });
}
} else {
newCategory.push(entry);
}
}
return {
updateOne: {
filter: { _id: doc._id },
update: {
$set: {
category: newCategory,
mentions
}
}
}
};
});
const result = await ap_timeline.bulkWrite(ops, { ordered: false });
const updated = result.modifiedCount || 0;
// Mark migration complete
await cacheSet(MIGRATION_KEY, { completed: true, date: new Date().toISOString(), updated });
return { skipped: false, updated };
}