cache recent favorites, renotes, and reactions in the connection to speed up rePackNote

This commit is contained in:
Hazelnoot 2025-06-23 16:05:57 -04:00
parent 4c2a0fed63
commit bd22ae0d80
3 changed files with 86 additions and 23 deletions

View file

@ -315,19 +315,14 @@ export class NoteEntityService implements OnModuleInit {
@bindThis
public async populateMyRenotes(notes: Packed<'Note'>[], meId: string, _hint_?: {
myRenotes: Map<string, boolean>;
myRenotes: Set<string>;
}): Promise<Set<string>> {
const fetchedRenotes = new Set<string>();
const toFetch = new Set<string>();
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<string, boolean>;
myFavorites: Set<string>;
}): Promise<Set<string>> {
const fetchedFavorites = new Set<string>();
const toFetch = new Set<string>();
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);

View file

@ -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<string> = new Set();
public userMutedInstances: Set<string> = new Set();
public userMutedThreads: Set<string> = new Set();
public myRecentReactions: Map<string, string> = new Map();
public myRecentRenotes: Set<string> = new Set();
public myRecentFavorites: Set<string> = 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

View file

@ -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),
]);