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 @bindThis
public async populateMyRenotes(notes: Packed<'Note'>[], meId: string, _hint_?: { public async populateMyRenotes(notes: Packed<'Note'>[], meId: string, _hint_?: {
myRenotes: Map<string, boolean>; myRenotes: Set<string>;
}): Promise<Set<string>> { }): Promise<Set<string>> {
const fetchedRenotes = new Set<string>(); const fetchedRenotes = new Set<string>();
const toFetch = new Set<string>(); const toFetch = new Set<string>();
if (_hint_) { if (_hint_) {
for (const note of notes) { for (const note of notes) {
const fromHint = _hint_.myRenotes.get(note.id); if (_hint_.myRenotes.has(note.id)) {
// null means we know there's no renote, so just skip it.
if (fromHint === false) continue;
if (fromHint) {
fetchedRenotes.add(note.id); fetchedRenotes.add(note.id);
} else { } else {
toFetch.add(note.id); toFetch.add(note.id);
@ -355,19 +350,14 @@ export class NoteEntityService implements OnModuleInit {
@bindThis @bindThis
public async populateMyFavorites(notes: Packed<'Note'>[], meId: string, _hint_?: { public async populateMyFavorites(notes: Packed<'Note'>[], meId: string, _hint_?: {
myFavorites: Map<string, boolean>; myFavorites: Set<string>;
}): Promise<Set<string>> { }): Promise<Set<string>> {
const fetchedFavorites = new Set<string>(); const fetchedFavorites = new Set<string>();
const toFetch = new Set<string>(); const toFetch = new Set<string>();
if (_hint_) { if (_hint_) {
for (const note of notes) { for (const note of notes) {
const fromHint = _hint_.myFavorites.get(note.id); if (_hint_.myFavorites.has(note.id)) {
// null means we know there's no favorite, so just skip it.
if (fromHint === false) continue;
if (fromHint) {
fetchedFavorites.add(note.id); fetchedFavorites.add(note.id);
} else { } else {
toFetch.add(note.id); 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 type { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.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 type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { isJsonObject } from '@/misc/json-value.js'; import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.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 { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import type Channel from './channel.js'; import type Channel from './channel.js';
@ -43,14 +46,27 @@ export default class Connection {
public userIdsWhoMeMutingRenotes: Set<string> = new Set(); public userIdsWhoMeMutingRenotes: Set<string> = new Set();
public userMutedInstances: Set<string> = new Set(); public userMutedInstances: Set<string> = new Set();
public userMutedThreads: 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 fetchIntervalId: NodeJS.Timeout | null = null;
private closingConnection = false; private closingConnection = false;
private logger: Logger; private logger: Logger;
constructor( 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 channelsService: ChannelsService,
private notificationService: NotificationService, private notificationService: NotificationService,
private cacheService: CacheService, public readonly cacheService: CacheService,
private readonly queryService: QueryService,
private channelFollowingService: ChannelFollowingService, private channelFollowingService: ChannelFollowingService,
loggerService: LoggerService, loggerService: LoggerService,
@ -68,7 +84,7 @@ export default class Connection {
@bindThis @bindThis
public async fetch() { public async fetch() {
if (this.user == null) return; 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.userProfileCache.fetch(this.user.id),
this.cacheService.userFollowingsCache.fetch(this.user.id), this.cacheService.userFollowingsCache.fetch(this.user.id),
this.channelFollowingService.userFollowingChannelsCache.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.userBlockedCache.fetch(this.user.id),
this.cacheService.renoteMutingsCache.fetch(this.user.id), this.cacheService.renoteMutingsCache.fetch(this.user.id),
this.cacheService.threadMutingsCache.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.userProfile = userProfile;
this.following = following; this.following = following;
@ -85,6 +120,9 @@ export default class Connection {
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes; this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
this.userMutedInstances = new Set(userProfile.mutedInstances); this.userMutedInstances = new Set(userProfile.mutedInstances);
this.userMutedThreads = threadMutings; 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 @bindThis

View file

@ -34,18 +34,35 @@ export default abstract class Channel {
return this.connection.userProfile; return this.connection.userProfile;
} }
protected get cacheService() {
return this.connection.cacheService;
}
/**
* @deprecated use cacheService.userFollowingsCache to avoid stale data
*/
protected get following() { protected get following() {
return this.connection.following; return this.connection.following;
} }
/**
* TODO use onChange to keep these in sync?
* @deprecated use cacheService.userMutingsCache to avoid stale data
*/
protected get userIdsWhoMeMuting() { protected get userIdsWhoMeMuting() {
return this.connection.userIdsWhoMeMuting; return this.connection.userIdsWhoMeMuting;
} }
/**
* @deprecated use cacheService.renoteMutingsCache to avoid stale data
*/
protected get userIdsWhoMeMutingRenotes() { protected get userIdsWhoMeMutingRenotes() {
return this.connection.userIdsWhoMeMutingRenotes; return this.connection.userIdsWhoMeMutingRenotes;
} }
/**
* @deprecated use cacheService.userBlockedCache to avoid stale data
*/
protected get userIdsWhoBlockingMe() { protected get userIdsWhoBlockingMe() {
return this.connection.userIdsWhoBlockingMe; return this.connection.userIdsWhoBlockingMe;
} }
@ -54,6 +71,9 @@ export default abstract class Channel {
return this.connection.userMutedInstances; return this.connection.userMutedInstances;
} }
/**
* @deprecated use cacheService.threadMutingsCache to avoid stale data
*/
protected get userMutedThreads() { protected get userMutedThreads() {
return this.connection.userMutedThreads; return this.connection.userMutedThreads;
} }
@ -66,6 +86,18 @@ export default abstract class Channel {
return this.connection.subscriber; 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. * 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. // Hide notes before everything else, since this modifies fields that the other functions will check.
await this.noteEntityService.hideNotes(notes, this.user.id); 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([ const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings] = await Promise.all([
this.noteEntityService.populateMyReactions(notes, this.user.id), this.noteEntityService.populateMyReactions(notes, this.user.id, {
this.noteEntityService.populateMyRenotes(notes, this.user.id), myReactions: this.myRecentReactions,
this.noteEntityService.populateMyFavorites(notes, this.user.id), }),
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.populateMyTheadMutings(notes, this.user.id),
this.noteEntityService.populateMyNoteMutings(notes, this.user.id), this.noteEntityService.populateMyNoteMutings(notes, this.user.id),
]); ]);