diff --git a/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js b/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js new file mode 100644 index 000000000..fe0ea282d --- /dev/null +++ b/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + + +export class NoteReactionAndUserPairCache1697673894459 { + name = 'NoteReactionAndUserPairCache1697673894459' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "reactionAndUserPairCache" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAndUserPairCache"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 30d7f3e76..2c00418c4 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -584,7 +584,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // Pack the note - const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true }); + const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); this.globalEventService.publishNotesStream(noteObj); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 1458e2b17..edf433025 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -187,6 +187,9 @@ export class ReactionService { await this.notesRepository.createQueryBuilder().update() .set({ reactions: () => sql, + ...(note.reactionAndUserPairCache.length < 10 ? { + reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}:${reaction}')`, + } : {}), }) .where('id = :id', { id: note.id }) .execute(); @@ -293,6 +296,7 @@ export class ReactionService { await this.notesRepository.createQueryBuilder().update() .set({ reactions: () => sql, + reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}:${exist.reaction}')`, }) .where('id = :id', { id: note.id }) .execute(); diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 66dbb6616..b46b5528a 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -170,26 +170,37 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public async populateMyReaction(noteId: MiNote['id'], meId: MiUser['id'], _hint_?: { - myReactions: Map; + public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: { + myReactions: Map; }) { if (_hint_?.myReactions) { - const reaction = _hint_.myReactions.get(noteId); + const reaction = _hint_.myReactions.get(note.id); if (reaction) { - return this.reactionService.convertLegacyReaction(reaction.reaction); + return this.reactionService.convertLegacyReaction(reaction); + } else { + return undefined; + } + } + + const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); + if (reactionsCount === 0) return undefined; + if (note.reactionAndUserPairCache && reactionsCount <= note.reactionAndUserPairCache.length) { + const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); + if (pair) { + return this.reactionService.convertLegacyReaction(pair.split(':')[1]); } else { return undefined; } } // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない - if (this.idService.parse(noteId).date.getTime() + 2000 > Date.now()) { + if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) { return undefined; } const reaction = await this.noteReactionsRepository.findOneBy({ userId: meId, - noteId: noteId, + noteId: note.id, }); if (reaction) { @@ -275,8 +286,9 @@ export class NoteEntityService implements OnModuleInit { options?: { detail?: boolean; skipHide?: boolean; + withReactionAndUserPairCache?: boolean; _hint_?: { - myReactions: Map; + myReactions: Map; packedFiles: Map | null>; }; }, @@ -284,6 +296,7 @@ export class NoteEntityService implements OnModuleInit { const opts = Object.assign({ detail: true, skipHide: false, + withReactionAndUserPairCache: false, }, options); const meId = me ? me.id : null; @@ -324,6 +337,7 @@ export class NoteEntityService implements OnModuleInit { repliesCount: note.repliesCount, reactions: this.reactionService.convertLegacyReactions(note.reactions), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), + reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, tags: note.tags.length > 0 ? note.tags : undefined, fileIds: note.fileIds, @@ -346,18 +360,20 @@ export class NoteEntityService implements OnModuleInit { reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { detail: false, + withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, }) : undefined, renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { detail: true, + withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, }) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, ...(meId && Object.keys(note.reactions).length > 0 ? { - myReaction: this.populateMyReaction(note.id, meId, options?._hint_), + myReaction: this.populateMyReaction(note, meId, options?._hint_), } : {}), } : {}), }); @@ -381,19 +397,48 @@ export class NoteEntityService implements OnModuleInit { if (notes.length === 0) return []; const meId = me ? me.id : null; - const myReactionsMap = new Map(); + const myReactionsMap = new Map(); if (meId) { - const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); + const idsNeedFetchMyReaction = new Set(); + // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない const oldId = this.idService.gen(Date.now() - 2000); - const targets = [...notes.filter(n => (n.id < oldId) && (Object.keys(n.reactions).length > 0)).map(n => n.id), ...renoteIds]; - const myReactions = targets.length > 0 ? await this.noteReactionsRepository.findBy({ + + for (const note of notes) { + if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote + const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0); + if (reactionsCount === 0) { + myReactionsMap.set(note.renote.id, null); + } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) { + const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId)); + myReactionsMap.set(note.renote.id, pair ? pair.split(':')[1] : null); + } else { + idsNeedFetchMyReaction.add(note.renote.id); + } + } else { + if (note.id < oldId) { + const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); + if (reactionsCount === 0) { + myReactionsMap.set(note.id, null); + } else if (reactionsCount <= note.reactionAndUserPairCache.length) { + const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); + myReactionsMap.set(note.id, pair ? pair.split(':')[1] : null); + } else { + idsNeedFetchMyReaction.add(note.id); + } + } else { + myReactionsMap.set(note.id, null); + } + } + } + + const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({ userId: meId, - noteId: In(targets), + noteId: In(Array.from(idsNeedFetchMyReaction)), }) : []; - for (const target of targets) { - myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); + for (const id of idsNeedFetchMyReaction) { + myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null); } } diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index ac7f57d5d..a4358b9ba 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -164,6 +164,11 @@ export class MiNote { }) public mentionedRemoteUsers: string; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public reactionAndUserPairCache: string[]; + @Column('varchar', { length: 128, array: true, default: '{}', }) diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 2caf0d0c3..38c0054b5 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -174,6 +174,14 @@ export const packedNoteSchema = { type: 'string', optional: true, nullable: false, }, + reactionAndUserPairCache: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, myReaction: { type: 'object', diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 4d85e1cee..57034231a 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -47,7 +47,7 @@ class ChannelChannel extends Channel { if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } } diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 18b8e8245..553c44071 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -73,7 +73,7 @@ class GlobalTimelineChannel extends Channel { if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } } diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index fb9226413..f30b29cfd 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -52,7 +52,7 @@ class HashtagChannel extends Channel { if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } } diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 691484153..2b235b982 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -75,7 +75,7 @@ class HomeTimelineChannel extends Channel { if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } } diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index c353aaac0..4e9091208 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -89,7 +89,8 @@ class HybridTimelineChannel extends Channel { if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); + console.log(note.renote.reactionAndUserPairCache); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } } diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 0823799d2..9dd05b9b0 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -72,7 +72,7 @@ class LocalTimelineChannel extends Channel { if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } } diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index a5a9d189b..6d83d4cb5 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -104,7 +104,7 @@ class UserListChannel extends Channel { if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } }