diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 7c976d8d8..ad4ef0c0d 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -28,6 +28,7 @@ import { } from "@/misc/populate-emojis.js"; import { db } from "@/db/postgre.js"; import { IdentifiableError } from "@/misc/identifiable-error.js"; +import { PackedUserCache } from "@/models/repositories/user.js"; export async function populatePoll(note: Note, meId: User["id"] | null) { const poll = await Polls.findOneByOrFail({ noteId: note.id }); @@ -173,6 +174,7 @@ export const NoteRepository = db.getRepository(Note).extend({ myRenotes: Map; }; }, + userCache: PackedUserCache = Users.getFreshPackedUserCache(), ): Promise> { const opts = Object.assign( { @@ -221,7 +223,7 @@ export const NoteRepository = db.getRepository(Note).extend({ id: note.id, createdAt: note.createdAt.toISOString(), userId: note.userId, - user: Users.pack(note.user ?? note.userId, me, { + user: Users.packCached(note.user ?? note.userId, userCache, me, { detail: false, }), text: text, @@ -265,14 +267,14 @@ export const NoteRepository = db.getRepository(Note).extend({ ? this.tryPack(note.reply || note.replyId, me, { detail: false, _hint_: options?._hint_, - }) + }, userCache) : undefined, renote: note.renoteId ? this.pack(note.renote || note.renoteId, me, { detail: true, _hint_: options?._hint_, - }) + }, userCache) : undefined, } : {}), @@ -309,9 +311,10 @@ export const NoteRepository = db.getRepository(Note).extend({ myRenotes: Map; }; }, + userCache: PackedUserCache = Users.getFreshPackedUserCache(), ): Promise | undefined> { try { - return await this.pack(src, me, options); + return await this.pack(src, me, options, userCache); } catch { return undefined; } diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 3a8b7874a..37a84779a 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -7,7 +7,6 @@ import type { Packed } from "@/misc/schema.js"; import type { Promiseable } from "@/prelude/await-all.js"; import { awaitAll } from "@/prelude/await-all.js"; import { populateEmojis } from "@/misc/populate-emojis.js"; -import { getAntennas } from "@/misc/antenna-cache.js"; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from "@/const.js"; import { Cache } from "@/misc/cache.js"; import { db } from "@/db/postgre.js"; @@ -37,6 +36,7 @@ import { UserSecurityKeys, } from "../index.js"; import type { Instance } from "../entities/instance.js"; +import AsyncLock from "async-lock"; const userInstanceCache = new Cache( "userInstance", @@ -59,6 +59,11 @@ type IsMeAndIsUserDetailed< const ajv = new Ajv(); +export type PackedUserCache = { + locks: AsyncLock; + results: IsMeAndIsUserDetailed[]; +} + const localUsernameSchema = { type: "string", pattern: /^\w{1,20}$/.toString().slice(1, -1), @@ -366,6 +371,37 @@ export const UserRepository = db.getRepository(User).extend({ return `${config.url}/identicon/${userId}`; }, + getFreshPackedUserCache(): PackedUserCache { + return { + locks: new AsyncLock(), + results: [], + }; + }, + + async packCached< + ExpectsMe extends boolean | null = null, + D extends boolean = false, + >( + src: User["id"] | User, + cache: PackedUserCache, + me?: { id: User["id"] } | null | undefined, + options?: { + detail?: D; + includeSecrets?: boolean; + isPrivateMode?: boolean; + }, + ): Promise> { + const id = typeof src === "object" ? src.id : src; + return cache.locks.acquire(id, async () => { + const result = cache.results.find(p => p.id === id); + if (result) return result as IsMeAndIsUserDetailed + return this.pack(src, me, options).then(result => { + cache.results.push(result); + return result; + }); + }); + }, + async pack< ExpectsMe extends boolean | null = null, D extends boolean = false, @@ -638,8 +674,9 @@ export const UserRepository = db.getRepository(User).extend({ detail?: D; includeSecrets?: boolean; }, + cache?: PackedUserCache, ): Promise[]> { - return Promise.all(users.map((u) => this.pack(u, me, options))); + return Promise.all(users.map((u) => this.packCached(u, cache ?? this.getFreshPackedUserCache(), me, options))); }, isLocalUser,