diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 0af65b81b1..daf0894cfd 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1134,7 +1134,8 @@ export class NoteCreateService implements OnApplicationShutdown { } private async updateLatestNote(note: MiNote) { - // Ignore DMs + // Ignore DMs. + // Followers-only posts are *included*, as this table is used to back the "following" feed. if (note.visibility === 'specified') return; // Ignore pure renotes @@ -1143,7 +1144,7 @@ export class NoteCreateService implements OnApplicationShutdown { // Make sure that this isn't an *older* post. // We can get older posts through replies, lookups, etc. const currentLatest = await this.latestNotesRepository.findOneBy({ userId: note.userId }); - if (currentLatest != null && currentLatest.userId >= note.id) return; + if (currentLatest != null && currentLatest.noteId >= note.id) return; // Record this as the latest note for the given user const latestNote = new LatestNote({ diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index de753a3aa2..3f86f41942 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -240,6 +240,10 @@ export class NoteDeleteService { // If it's a DM, then it can't possibly be the latest note so we can safely skip this. if (note.visibility === 'specified') return; + // Check if the deleted note was possibly the latest for the user + const hasLatestNote = await this.latestNotesRepository.existsBy({ userId: note.userId }); + if (hasLatestNote) return; + // Find the newest remaining note for the user. // We exclude DMs and pure renotes. const nextLatest = await this.notesRepository @@ -269,12 +273,14 @@ export class NoteDeleteService { noteId: nextLatest.id, }); - // We use an upsert because this deleted note might not have been the newest. - // In that case, the latest note may already be populated for this user. - // We want postgres to do nothing instead of replacing the value or returning an error. - await this.latestNotesRepository.upsert(latestNote, { - conflictPaths: ['userId'], - skipUpdateIfNoValuesChanged: true, - }); + // When inserting the latest note, it's possible that another worker has "raced" the insert and already added a newer note. + // We must use orIgnore() to ensure that the query ignores conflicts, otherwise an exception may be thrown. + await this.latestNotesRepository + .createQueryBuilder('latest') + .insert() + .into(LatestNote) + .values(latestNote) + .orIgnore() + .execute(); } } diff --git a/packages/frontend/src/components/FollowingFeedEntry.vue b/packages/frontend/src/components/FollowingFeedEntry.vue index 29434de021..7f5abaa9cc 100644 --- a/packages/frontend/src/components/FollowingFeedEntry.vue +++ b/packages/frontend/src/components/FollowingFeedEntry.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only