[mastodon-client] Handle html cache misses properly

This commit is contained in:
Laura Hausmann 2023-11-27 00:57:15 +01:00
parent 7ab7edeefd
commit 58d70d005f
No known key found for this signature in database
GPG Key ID: D044E84C5BE01605
2 changed files with 66 additions and 5 deletions

View File

@ -23,7 +23,7 @@ import { PollConverter } from "@/server/api/mastodon/converters/poll.js";
import { populatePoll } from "@/models/repositories/note.js"; import { populatePoll } from "@/models/repositories/note.js";
import { FileConverter } from "@/server/api/mastodon/converters/file.js"; import { FileConverter } from "@/server/api/mastodon/converters/file.js";
import { awaitAll } from "@/prelude/await-all.js"; import { awaitAll } from "@/prelude/await-all.js";
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { In, IsNull } from "typeorm"; import { In, IsNull } from "typeorm";
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js"; import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
import { getStubMastoContext, MastoContext } from "@/server/api/mastodon/index.js"; import { getStubMastoContext, MastoContext } from "@/server/api/mastodon/index.js";
@ -308,14 +308,40 @@ export class NoteConverter {
return Promise.resolve(dbHit) return Promise.resolve(dbHit)
.then(res => { .then(res => {
if (res === null || (res.updatedAt?.getTime() !== note.updatedAt?.getTime())) { if (res === null || (res.updatedAt?.getTime() !== note.updatedAt?.getTime())) {
this.prewarmCache(note); return this.dbCacheMiss(note, ctx);
return null;
} }
return res; return res;
}) })
.then(hit => hit?.updatedAt === note.updatedAt ? hit?.content ?? null : null); .then(hit => hit?.updatedAt === note.updatedAt ? hit?.content ?? null : null);
} }
private static async dbCacheMiss(note: Note, ctx: MastoContext): Promise<HtmlNoteCacheEntry | null> {
const identifier = `${note.id}:${(note.updatedAt ?? note.createdAt).getTime()}`;
const cache = ctx.cache as AccountCache;
return cache.locks.acquire(identifier, async () => {
const cachedContent = await this.noteContentHtmlCache.get(identifier);
if (cachedContent !== undefined) {
return { content: cachedContent } as HtmlNoteCacheEntry;
}
const quoteUri = note.renote
? isQuote(note)
? (note.renote.url ?? note.renote.uri ?? `${config.url}/notes/${note.renote.id}`)
: null
: null;
const text = note.text !== null ? quoteUri !== null ? note.text.replaceAll(`RE: ${quoteUri}`, '').replaceAll(quoteUri, '').trimEnd() : note.text : null;
const content = text !== null
? MfmHelpers.toHtml(mfm.parse(text), JSON.parse(note.mentionedRemoteUsers), note.userHost, false, quoteUri)
.then(p => p ?? escapeMFM(text))
: null;
HtmlNoteCacheEntries.upsert({ noteId: note.id, updatedAt: note.updatedAt, content: await content }, ["noteId"]);
await this.noteContentHtmlCache.set(identifier, await content);
return { content } as HtmlNoteCacheEntry;
});
}
public static async prewarmCache(note: Note): Promise<void> { public static async prewarmCache(note: Note): Promise<void> {
if (!config.htmlCache?.prewarm) return; if (!config.htmlCache?.prewarm) return;
const identifier = `${note.id}:${(note.updatedAt ?? note.createdAt).getTime()}`; const identifier = `${note.id}:${(note.updatedAt ?? note.createdAt).getTime()}`;

View File

@ -244,13 +244,48 @@ export class UserConverter {
return Promise.resolve(dbHit) return Promise.resolve(dbHit)
.then(res => { .then(res => {
if (res === null || (res.updatedAt.getTime() !== (user.lastFetchedAt ?? user.createdAt).getTime())) { if (res === null || (res.updatedAt.getTime() !== (user.lastFetchedAt ?? user.createdAt).getTime())) {
this.prewarmCache(user, profile); return this.dbCacheMiss(user, profile, ctx);
return null;
} }
return res; return res;
}); });
} }
private static async dbCacheMiss(user: User, profile: UserProfile | null, ctx: MastoContext): Promise<HtmlUserCacheEntry | null> {
const identifier = `${user.id}:${(user.lastFetchedAt ?? user.createdAt).getTime()}`;
const cache = ctx.cache as AccountCache;
return cache.locks.acquire(identifier, async () => {
const cachedBio = await this.userBioHtmlCache.get(identifier);
const cachedFields = await this.userFieldsHtmlCache.get(identifier);
if (cachedBio !== undefined && cachedFields !== undefined) {
return { bio: cachedBio, fields: cachedFields } as HtmlUserCacheEntry;
}
if (profile === undefined) {
profile = await UserProfiles.findOneBy({ userId: user.id });
}
let bio: string | null | Promise<string | null> | undefined = cachedBio;
let fields: MastodonEntity.Field[] | Promise<MastodonEntity.Field[]> | undefined = cachedFields;
if (bio === undefined) {
bio = MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, user.host)
.then(p => p ?? escapeMFM(profile?.description ?? ""))
.then(p => p !== '<p></p>' ? p : null);
}
if (fields === undefined) {
fields = Promise.all(profile!.fields.map(async p => this.encodeField(p, user.host, profile!.mentions)) ?? []);
}
HtmlUserCacheEntries.upsert({ userId: user.id, updatedAt: user.lastFetchedAt ?? user.createdAt, bio: await bio, fields: await fields }, ["userId"]);
await this.userBioHtmlCache.set(identifier, await bio);
await this.userFieldsHtmlCache.set(identifier, await fields);
return { bio, fields } as HtmlUserCacheEntry;
});
}
public static async prewarmCache(user: User, profile?: UserProfile | null, oldProfile?: UserProfile | null): Promise<void> { public static async prewarmCache(user: User, profile?: UserProfile | null, oldProfile?: UserProfile | null): Promise<void> {
const identifier = `${user.id}:${(user.lastFetchedAt ?? user.createdAt).getTime()}`; const identifier = `${user.id}:${(user.lastFetchedAt ?? user.createdAt).getTime()}`;
if (profile !== null) { if (profile !== null) {