feat: consolidated patch-ap-mastodon-accounts (actor-cache-await, timeout-safe)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Consolidated patch: Mastodon account resolution fixes.
|
||||||
|
*
|
||||||
|
* Absorbs:
|
||||||
|
* - patch-ap-actor-cache-await
|
||||||
|
* Await the ap_actor_cache MongoDB write so the entry exists before the
|
||||||
|
* search response is sent, making follow reliable after server restarts.
|
||||||
|
*
|
||||||
|
* - patch-ap-resolve-account-timeout-safe
|
||||||
|
* Fix unhandled rejection crash when withTimeout() timer fires before the
|
||||||
|
* original promise settles, and parallelise collection count fetches with
|
||||||
|
* Promise.allSettled (max 5 s instead of worst-case 15 s).
|
||||||
|
*
|
||||||
|
* Both patches target:
|
||||||
|
* lib/mastodon/helpers/resolve-account.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { access, readFile, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
const AP_BASE = "@rmdes/indiekit-endpoint-activitypub";
|
||||||
|
const AP_ROOTS = [
|
||||||
|
`node_modules/${AP_BASE}`,
|
||||||
|
`node_modules/@indiekit/indiekit/node_modules/${AP_BASE}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
function apPath(rel) {
|
||||||
|
return AP_ROOTS.map(r => `${r}/${rel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileExists(p) {
|
||||||
|
try { await access(p); return true; } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPatch(filePath, marker, oldSnippet, newSnippet) {
|
||||||
|
if (!(await fileExists(filePath))) return "file_not_found";
|
||||||
|
const src = await readFile(filePath, "utf8");
|
||||||
|
if (src.includes(marker)) return "already_applied";
|
||||||
|
if (!src.includes(oldSnippet)) return "snippet_not_found";
|
||||||
|
await writeFile(filePath, src.replace(oldSnippet, newSnippet), "utf8");
|
||||||
|
return "applied";
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCRIPT = "patch-ap-mastodon-accounts";
|
||||||
|
|
||||||
|
const PATCHES = [
|
||||||
|
{
|
||||||
|
name: "ap-actor-cache-await",
|
||||||
|
files: apPath("lib/mastodon/helpers/resolve-account.js"),
|
||||||
|
marker: "// [patch] ap-actor-cache-await",
|
||||||
|
oldSnippet: ` // Persist actor URL mapping to MongoDB so follow/unfollow survives server restarts
|
||||||
|
if (collections?.ap_actor_cache && actorUrl) {
|
||||||
|
const hashId = remoteActorId(actorUrl);
|
||||||
|
collections.ap_actor_cache.updateOne(
|
||||||
|
{ _id: hashId },
|
||||||
|
{ $set: { actorUrl, updatedAt: new Date() } },
|
||||||
|
{ upsert: true },
|
||||||
|
).catch(() => {}); // fire-and-forget, non-fatal
|
||||||
|
}`,
|
||||||
|
newSnippet: ` // Persist actor URL mapping to MongoDB so follow/unfollow survives server restarts
|
||||||
|
// [patch] ap-actor-cache-await
|
||||||
|
if (collections?.ap_actor_cache && actorUrl) {
|
||||||
|
const hashId = remoteActorId(actorUrl);
|
||||||
|
await collections.ap_actor_cache.updateOne(
|
||||||
|
{ _id: hashId },
|
||||||
|
{ $set: { actorUrl, updatedAt: new Date() } },
|
||||||
|
{ upsert: true },
|
||||||
|
).catch(() => {}); // non-fatal, but now awaited so entry exists before response
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ap-resolve-account-timeout-safe",
|
||||||
|
files: apPath("lib/mastodon/helpers/resolve-account.js"),
|
||||||
|
marker: "// [patch] ap-resolve-account-timeout-safe",
|
||||||
|
oldSnippet: ` const withTimeout = (promise, ms = 5000) =>
|
||||||
|
Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))]);
|
||||||
|
|
||||||
|
let followersCount = 0;
|
||||||
|
let followingCount = 0;
|
||||||
|
let statusesCount = 0;
|
||||||
|
try {
|
||||||
|
const followers = await withTimeout(actor.getFollowers());
|
||||||
|
if (followers?.totalItems != null) followersCount = followers.totalItems;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
try {
|
||||||
|
const following = await withTimeout(actor.getFollowing());
|
||||||
|
if (following?.totalItems != null) followingCount = following.totalItems;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
try {
|
||||||
|
const outbox = await withTimeout(actor.getOutbox());
|
||||||
|
if (outbox?.totalItems != null) statusesCount = outbox.totalItems;
|
||||||
|
} catch { /* ignore */ }`,
|
||||||
|
newSnippet: ` const withTimeout = (promise, ms = 5000) => { // [patch] ap-resolve-account-timeout-safe
|
||||||
|
const abort = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms));
|
||||||
|
promise.catch(() => {}); // suppress unhandled rejection if timeout settles first
|
||||||
|
return Promise.race([promise, abort]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch collection counts in parallel (max 5 s each) [patch] ap-resolve-account-timeout-safe
|
||||||
|
const [followersResult, followingResult, outboxResult] = await Promise.allSettled([
|
||||||
|
withTimeout(actor.getFollowers()),
|
||||||
|
withTimeout(actor.getFollowing()),
|
||||||
|
withTimeout(actor.getOutbox()),
|
||||||
|
]);
|
||||||
|
const followersCount = followersResult.status === "fulfilled" && followersResult.value?.totalItems != null ? followersResult.value.totalItems : 0;
|
||||||
|
const followingCount = followingResult.status === "fulfilled" && followingResult.value?.totalItems != null ? followingResult.value.totalItems : 0;
|
||||||
|
const statusesCount = outboxResult.status === "fulfilled" && outboxResult.value?.totalItems != null ? outboxResult.value.totalItems : 0;`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
for (const p of PATCHES) {
|
||||||
|
let done = false;
|
||||||
|
for (const f of p.files) {
|
||||||
|
const r = await applyPatch(f, p.marker, p.oldSnippet, p.newSnippet);
|
||||||
|
if (r === "applied") {
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: applied ${p.name} to ${f}`);
|
||||||
|
total++; done = true; break;
|
||||||
|
} else if (r === "already_applied") {
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: ${p.name} already applied in ${f}`);
|
||||||
|
done = true; break;
|
||||||
|
} else if (r === "snippet_not_found") {
|
||||||
|
console.warn(`[postinstall] ${SCRIPT}: ${p.name} — snippet not found in ${f}, skipping`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!done) console.log(`[postinstall] ${SCRIPT}: ${p.name} — no target file found`);
|
||||||
|
}
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);
|
||||||
Reference in New Issue
Block a user