From 4bfc6e21208edc5869dd3a607ce84261de293c46 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 3 May 2026 12:36:28 +0200 Subject: [PATCH] =?UTF-8?q?Add=20patch:=20followers/following=20countDocum?= =?UTF-8?q?ents()=20=E2=86=92=20estimatedDocumentCount()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit federation-setup.js calls countDocuments() with no filter 4 times for the AP followers/following collection dispatchers (pagination totalItems + counter). All are wrapped in cachedQuery() but on cache miss they run full aggregate scans. estimatedDocumentCount() reads collection metadata — O(1), no scan. --- .../patch-ap-collection-count-estimate.mjs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 scripts/patch-ap-collection-count-estimate.mjs diff --git a/scripts/patch-ap-collection-count-estimate.mjs b/scripts/patch-ap-collection-count-estimate.mjs new file mode 100644 index 00000000..aeb567af --- /dev/null +++ b/scripts/patch-ap-collection-count-estimate.mjs @@ -0,0 +1,77 @@ +/** + * Patch: replace countDocuments() with estimatedDocumentCount() in + * federation-setup.js followers/following collection dispatchers. + * + * All four calls are already wrapped in cachedQuery(), but on cache miss + * countDocuments() runs a full aggregate scan. estimatedDocumentCount() + * reads collection metadata — O(1). + * + * Covers: ap_followers (×2) and ap_following (×2) in collection pagination + * and counter dispatchers. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", +]; + +const MARKER = "// [patch] collection-count-estimate"; + +const replacements = [ + { + old: ` const t = await collections.ap_followers.countDocuments(); + return [d, t];`, + new: ` const t = await collections.ap_followers.estimatedDocumentCount(); ${MARKER} + return [d, t];`, + }, + { + old: ` return await collections.ap_followers.countDocuments();`, + new: ` return await collections.ap_followers.estimatedDocumentCount(); ${MARKER}`, + }, + { + old: ` const t = await collections.ap_following.countDocuments(); + return [d, t];`, + new: ` const t = await collections.ap_following.estimatedDocumentCount(); ${MARKER} + return [d, t];`, + }, + { + old: ` return await collections.ap_following.countDocuments();`, + new: ` return await collections.ap_following.estimatedDocumentCount(); ${MARKER}`, + }, +]; + +async function exists(p) { + try { await access(p); return true; } catch { return false; } +} + +let patched = false; +for (const filePath of candidates) { + if (!(await exists(filePath))) continue; + let src = await readFile(filePath, "utf8"); + if (src.includes(MARKER)) { + console.log(`[postinstall] patch-ap-collection-count-estimate: already applied in ${filePath}`); + patched = true; + break; + } + let changed = 0; + for (const { old, new: replacement } of replacements) { + if (src.includes(old)) { + src = src.replace(old, replacement); + changed++; + } + } + if (changed === 0) { + console.log(`[postinstall] patch-ap-collection-count-estimate: target snippets not found in ${filePath}`); + continue; + } + await writeFile(filePath, src, "utf8"); + console.log(`[postinstall] patch-ap-collection-count-estimate: applied ${changed}/4 replacements in ${filePath}`); + patched = true; + break; +} + +if (!patched) { + console.log("[postinstall] patch-ap-collection-count-estimate: no target file found"); +}