fix cascaded note deletion and make it more efficient

This commit is contained in:
Hazelnoot 2025-06-24 11:47:21 -04:00
parent d06b2353b5
commit cad059f039

View file

@ -79,10 +79,18 @@ export class NoteDeleteService {
if (note.replyId) { if (note.replyId) {
promises.push(this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1)); promises.push(this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1));
} else if (isPureRenote(note)) {
promises.push(this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1));
} }
if (isPureRenote(note)) { const cascadeReplies = cascadingNotes.filter(cascade => cascade.replyId != null);
promises.push(this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1)); const cascadeRenotes = cascadingNotes.filter(cascade => cascade.renoteId != null);
if (cascadeReplies.length > 0) {
promises.push(this.notesRepository.decrement({ id: In(cascadeReplies.map(cascade => cascade.replyId)) }, 'repliesCount', 1));
}
if (cascadeRenotes.length > 0) {
promises.push(this.notesRepository.decrement({ id: In(cascadeRenotes.map(cascade => cascade.renoteId)) }, 'renoteCount', 1));
} }
if (!quiet) { if (!quiet) {
@ -90,6 +98,12 @@ export class NoteDeleteService {
deletedAt: deletedAt, deletedAt: deletedAt,
})); }));
for (const cascade of cascadingNotes) {
promises.push(this.globalEventService.publishNoteStream(cascade.id, 'deleted', {
deletedAt: deletedAt,
}));
}
//#region ローカルの投稿なら削除アクティビティを配送 //#region ローカルの投稿なら削除アクティビティを配送
if (isLocalUser(user) && !note.localOnly) { if (isLocalUser(user) && !note.localOnly) {
const renote = isPureRenote(note) const renote = isPureRenote(note)
@ -118,6 +132,13 @@ export class NoteDeleteService {
this.perUserNotesChart.update(user, note, false); this.perUserNotesChart.update(user, note, false);
} }
for (const cascade of cascadingNotes) {
this.notesChart.update(cascade, false);
if (this.meta.enableChartsForRemoteUser || (cascade.user.host == null)) {
this.perUserNotesChart.update(cascade.user, cascade, false);
}
}
if (!isPureRenote(note)) { if (!isPureRenote(note)) {
// Decrement notes count (user) // Decrement notes count (user)
promises.push(this.decNotesCountOfUser(user)); promises.push(this.decNotesCountOfUser(user));
@ -125,6 +146,13 @@ export class NoteDeleteService {
promises.push(this.queueService.createMarkUserUpdatedJob(user.id)); promises.push(this.queueService.createMarkUserUpdatedJob(user.id));
} }
for (const cascade of cascadingNotes) {
if (!isPureRenote(cascade)) {
promises.push(this.decNotesCountOfUser(cascade.user));
}
// Don't mark cascaded user as updated (active)
}
if (this.meta.enableStatsForFederatedInstances) { if (this.meta.enableStatsForFederatedInstances) {
if (isRemoteUser(user)) { if (isRemoteUser(user)) {
if (!isPureRenote(note)) { if (!isPureRenote(note)) {
@ -135,6 +163,18 @@ export class NoteDeleteService {
this.instanceChart.updateNote(user.host, note, false); this.instanceChart.updateNote(user.host, note, false);
} }
} }
for (const cascade of cascadingNotes) {
if (this.userEntityService.isRemoteUser(cascade.user)) {
if (!isPureRenote(cascade)) {
const i = await this.federatedInstanceService.fetchOrRegister(cascade.user.host);
promises.push(this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1));
}
if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(cascade.user.host, cascade, false);
}
}
}
} }
} }
@ -189,26 +229,42 @@ export class NoteDeleteService {
} }
@bindThis @bindThis
private async findCascadingNotes(note: MiNote): Promise<MiNote[]> { private async findCascadingNotes(note: MiNote): Promise<(MiNote & { user: MiUser })[]> {
const recursive = async (noteId: string): Promise<MiNote[]> => { const cascadingNotes: MiNote[] = [];
const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => {
q.where('note.renoteId = :noteId', { noteId })
.andWhere('note.text IS NOT NULL');
}))
.leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany();
return [ /**
replies, * Finds all replies, quotes, and renotes of the given list of notes.
...await Promise.all(replies.map(reply => recursive(reply.id))), * These are the notes that will be CASCADE deleted when the origin note is deleted.
].flat(); *
* This works by operating in "layers" that radiate out from the origin note like a web.
* The process is roughly like this:
* 1. Find all immediate replies and renotes of the origin.
* 2. Find all immediate replies and renotes of the results from step one.
* 3. Repeat until step 2 returns no new results.
* 4. Collect all the step 2 results; those are the set of all cascading notes.
*/
const cascade = async (layer: MiNote[]): Promise<void> => {
const layerIds = layer.map(layer => layer.id);
const refs = await this.notesRepository.find({
where: [
{ replyId: In(layerIds) },
{ renoteId: In(layerIds) },
],
relations: { user: true },
});
// Stop when we reach the end of all threads
if (refs.length === 0) return;
cascadingNotes.push(...refs);
await cascade(refs);
}; };
const cascadingNotes: MiNote[] = await recursive(note.id); // Start with the origin, which should *not* be in the result set!
await cascade([note]);
return cascadingNotes; // Type cast is safe - we load the relation above.
return cascadingNotes as (MiNote & { user: MiUser })[];
} }
@bindThis @bindThis