diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 6720644197..029a0b6ebb 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -315,19 +315,14 @@ export class NoteEntityService implements OnModuleInit { @bindThis public async populateMyRenotes(notes: Packed<'Note'>[], meId: string, _hint_?: { - myRenotes: Map; + myRenotes: Set; }): Promise> { const fetchedRenotes = new Set(); const toFetch = new Set(); if (_hint_) { for (const note of notes) { - const fromHint = _hint_.myRenotes.get(note.id); - - // null means we know there's no renote, so just skip it. - if (fromHint === false) continue; - - if (fromHint) { + if (_hint_.myRenotes.has(note.id)) { fetchedRenotes.add(note.id); } else { toFetch.add(note.id); @@ -355,19 +350,14 @@ export class NoteEntityService implements OnModuleInit { @bindThis public async populateMyFavorites(notes: Packed<'Note'>[], meId: string, _hint_?: { - myFavorites: Map; + myFavorites: Set; }): Promise> { const fetchedFavorites = new Set(); const toFetch = new Set(); if (_hint_) { for (const note of notes) { - const fromHint = _hint_.myFavorites.get(note.id); - - // null means we know there's no favorite, so just skip it. - if (fromHint === false) continue; - - if (fromHint) { + if (_hint_.myFavorites.has(note.id)) { fetchedFavorites.add(note.id); } else { toFetch.add(note.id); diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 8ca473ff56..f46cc1880e 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -10,13 +10,16 @@ import type { Packed } from '@/misc/json-schema.js'; import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { MiFollowing, MiUserProfile } from '@/models/_.js'; +import type { MiFollowing, MiUserProfile, NoteFavoritesRepository, NoteReactionsRepository, NotesRepository } from '@/models/_.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { isJsonObject } from '@/misc/json-value.js'; import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; +import { Inject } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; @@ -43,14 +46,27 @@ export default class Connection { public userIdsWhoMeMutingRenotes: Set = new Set(); public userMutedInstances: Set = new Set(); public userMutedThreads: Set = new Set(); + public myRecentReactions: Map = new Map(); + public myRecentRenotes: Set = new Set(); + public myRecentFavorites: Set = new Set(); private fetchIntervalId: NodeJS.Timeout | null = null; private closingConnection = false; private logger: Logger; constructor( + @Inject(DI.noteReactionsRepository) + private readonly noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.notesRepository) + private readonly notesRepository: NotesRepository, + + @Inject(DI.noteFavoritesRepository) + private readonly noteFavoritesRepository: NoteFavoritesRepository, + private channelsService: ChannelsService, private notificationService: NotificationService, - private cacheService: CacheService, + public readonly cacheService: CacheService, + private readonly queryService: QueryService, private channelFollowingService: ChannelFollowingService, loggerService: LoggerService, @@ -68,7 +84,7 @@ export default class Connection { @bindThis public async fetch() { if (this.user == null) return; - const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes, threadMutings] = await Promise.all([ + const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes, threadMutings, myRecentReactions, myRecentFavorites, myRecentRenotes] = await Promise.all([ this.cacheService.userProfileCache.fetch(this.user.id), this.cacheService.userFollowingsCache.fetch(this.user.id), this.channelFollowingService.userFollowingChannelsCache.fetch(this.user.id), @@ -76,6 +92,25 @@ export default class Connection { this.cacheService.userBlockedCache.fetch(this.user.id), this.cacheService.renoteMutingsCache.fetch(this.user.id), this.cacheService.threadMutingsCache.fetch(this.user.id), + this.noteReactionsRepository.find({ + where: { userId: this.user.id }, + select: { noteId: true, reaction: true }, + order: { id: 'desc' }, + take: 100, + }), + this.noteFavoritesRepository.find({ + where: { userId: this.user.id }, + select: { noteId: true }, + order: { id: 'desc' }, + take: 100, + }), + this.queryService + .andIsRenote(this.notesRepository.createQueryBuilder('note'), 'note') + .andWhere({ userId: this.user.id }) + .orderBy({ id: 'DESC' }) + .limit(100) + .select('note.renoteId', 'renoteId') + .getRawMany<{ renoteId: string }>(), ]); this.userProfile = userProfile; this.following = following; @@ -85,6 +120,9 @@ export default class Connection { this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes; this.userMutedInstances = new Set(userProfile.mutedInstances); this.userMutedThreads = threadMutings; + this.myRecentReactions = new Map(myRecentReactions.map(r => [r.noteId, r.reaction])); + this.myRecentFavorites = new Set(myRecentFavorites.map(f => f.noteId )); + this.myRecentRenotes = new Set(myRecentRenotes.map(r => r.renoteId )); } @bindThis diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 4417d6841e..34680cf6d8 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -34,18 +34,35 @@ export default abstract class Channel { return this.connection.userProfile; } + protected get cacheService() { + return this.connection.cacheService; + } + + /** + * @deprecated use cacheService.userFollowingsCache to avoid stale data + */ protected get following() { return this.connection.following; } + /** + * TODO use onChange to keep these in sync? + * @deprecated use cacheService.userMutingsCache to avoid stale data + */ protected get userIdsWhoMeMuting() { return this.connection.userIdsWhoMeMuting; } + /** + * @deprecated use cacheService.renoteMutingsCache to avoid stale data + */ protected get userIdsWhoMeMutingRenotes() { return this.connection.userIdsWhoMeMutingRenotes; } + /** + * @deprecated use cacheService.userBlockedCache to avoid stale data + */ protected get userIdsWhoBlockingMe() { return this.connection.userIdsWhoBlockingMe; } @@ -54,6 +71,9 @@ export default abstract class Channel { return this.connection.userMutedInstances; } + /** + * @deprecated use cacheService.threadMutingsCache to avoid stale data + */ protected get userMutedThreads() { return this.connection.userMutedThreads; } @@ -66,6 +86,18 @@ export default abstract class Channel { return this.connection.subscriber; } + protected get myRecentReactions() { + return this.connection.myRecentReactions; + } + + protected get myRecentRenotes() { + return this.connection.myRecentRenotes; + } + + protected get myRecentFavorites() { + return this.connection.myRecentFavorites; + } + /** * Checks if a note is visible to the current user *excluding* blocks and mutes. */ @@ -161,13 +193,16 @@ export default abstract class Channel { // Hide notes before everything else, since this modifies fields that the other functions will check. await this.noteEntityService.hideNotes(notes, this.user.id); - // TODO cache reaction/renote/favorite hints in the connection. - // Those functions accept partial hints and will fetch anything else. - const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings] = await Promise.all([ - this.noteEntityService.populateMyReactions(notes, this.user.id), - this.noteEntityService.populateMyRenotes(notes, this.user.id), - this.noteEntityService.populateMyFavorites(notes, this.user.id), + this.noteEntityService.populateMyReactions(notes, this.user.id, { + myReactions: this.myRecentReactions, + }), + this.noteEntityService.populateMyRenotes(notes, this.user.id, { + myRenotes: this.myRecentRenotes, + }), + this.noteEntityService.populateMyFavorites(notes, this.user.id, { + myFavorites: this.myRecentFavorites, + }), this.noteEntityService.populateMyTheadMutings(notes, this.user.id), this.noteEntityService.populateMyNoteMutings(notes, this.user.id), ]);