From 449ea4b669252cff3dee0de0192ce4a353106444 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Mar 2021 17:35:47 +0900 Subject: perf: reduce query --- src/services/note/reaction/create.ts | 47 +++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) (limited to 'src/services/note') diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index adc96ddc1f..8de9f53a17 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -11,34 +11,41 @@ import { perUserReactionsChart } from '../../chart'; import { genId } from '../../../misc/gen-id'; import { createNotification } from '../../create-notification'; import deleteReaction from './delete'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; export default async (user: User, note: Note, reaction?: string) => { reaction = await toDbReaction(reaction, user.host); - const exist = await NoteReactions.findOne({ - noteId: note.id, - userId: user.id, - }); + let record; + + // Create reaction + try { + record = await NoteReactions.save({ + id: genId(), + createdAt: new Date(), + noteId: note.id, + userId: user.id, + reaction + }); + } catch (e) { + if (isDuplicateKeyValueError(e)) { + record = await NoteReactions.findOneOrFail({ + noteId: note.id, + userId: user.id, + }); - if (exist) { - if (exist.reaction !== reaction) { - // 別のリアクションがすでにされていたら置き換える - await deleteReaction(user, note); + if (record.reaction !== reaction) { + // 別のリアクションがすでにされていたら置き換える + await deleteReaction(user, note); + } else { + // 同じリアクションがすでにされていたら何もしない + return; + } } else { - // 同じリアクションがすでにされていたら何もしない - return; + throw e; } } - // Create reaction - const inserted = await NoteReactions.save({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - reaction - }); - // Increment reactions count const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; await Notes.createQueryBuilder().update() @@ -101,7 +108,7 @@ export default async (user: User, note: Note, reaction?: string) => { //#region 配信 if (Users.isLocalUser(user) && !note.localOnly) { - const content = renderActivity(await renderLike(inserted, note)); + const content = renderActivity(await renderLike(record, note)); const dm = new DeliverManager(user, content); if (note.userHost !== null) { const reactee = await Users.findOne(note.userId); -- cgit v1.2.3-freya From bffdfea58af44c2faa543ed98cfb878b6d283935 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Mar 2021 17:38:42 +0900 Subject: refactor --- src/services/note/reaction/create.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/services/note') diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 8de9f53a17..30a17ad3e9 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -12,11 +12,12 @@ import { genId } from '../../../misc/gen-id'; import { createNotification } from '../../create-notification'; import deleteReaction from './delete'; import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; +import { NoteReaction } from '../../../models/entities/note-reaction'; export default async (user: User, note: Note, reaction?: string) => { reaction = await toDbReaction(reaction, user.host); - let record; + let record: NoteReaction; // Create reaction try { -- cgit v1.2.3-freya From 65e7204ec94cccb53a66f681efa9c057bb9580a9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 19 Mar 2021 10:53:09 +0900 Subject: perf: myReaction の取得をまとめて行うように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related #6813 --- src/models/repositories/note.ts | 47 +++++++++++++++++++++++++++++++++--- src/services/note/reaction/create.ts | 1 + 2 files changed, 44 insertions(+), 4 deletions(-) (limited to 'src/services/note') diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 32552db2fe..43caaf94b2 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -9,6 +9,7 @@ import { toString } from '../../mfm/to-string'; import { parse } from '../../mfm/parse'; import { Emoji } from '../entities/emoji'; import { concat } from '../../prelude/array'; +import { NoteReaction } from '../entities/note-reaction'; export type PackedNote = SchemaType; @@ -83,6 +84,9 @@ export class NoteRepository extends Repository { options?: { detail?: boolean; skipHide?: boolean; + _hint_?: { + myReactions: Map; + }; } ): Promise { const opts = Object.assign({ @@ -188,6 +192,16 @@ export class NoteRepository extends Repository { } async function populateMyReaction() { + if (options?._hint_?.myReactions) { + const reaction = options._hint_.myReactions.get(note.id); + if (reaction) { + return convertLegacyReaction(reaction.reaction); + } else if (reaction === null) { + return undefined; + } + // 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない + } + const reaction = await NoteReactions.findOne({ userId: meId!, noteId: note.id, @@ -245,11 +259,13 @@ export class NoteRepository extends Repository { ...(opts.detail ? { reply: note.replyId ? this.pack(note.replyId, meId, { - detail: false + detail: false, + _hint_: options?._hint_ }) : undefined, renote: note.renoteId ? this.pack(note.renoteId, meId, { - detail: true + detail: true, + _hint_: options?._hint_ }) : undefined, poll: note.hasPoll ? populatePoll() : undefined, @@ -272,7 +288,7 @@ export class NoteRepository extends Repository { return packed; } - public packMany( + public async packMany( notes: (Note['id'] | Note)[], me?: User['id'] | User | null | undefined, options?: { @@ -280,7 +296,30 @@ export class NoteRepository extends Repository { skipHide?: boolean; } ) { - return Promise.all(notes.map(n => this.pack(n, me, options))); + if (notes.length === 0) return []; + + const meId = me ? typeof me === 'string' ? me : me.id : null; + const noteIds = notes.map(n => typeof n === 'object' ? n.id : n); + const myReactionsMap = new Map(); + if (meId) { + const renoteIds = notes.filter(n => (typeof n === 'object') && (n.renoteId != null)).map(n => (n as Note).renoteId!); + const targets = [...noteIds, ...renoteIds]; + const myReactions = await NoteReactions.find({ + userId: meId, + noteId: In(targets), + }); + + for (const target of targets) { + myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null); + } + } + + return await Promise.all(notes.map(n => this.pack(n, me, { + ...options, + _hint_: { + myReactions: myReactionsMap + } + }))); } } diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 30a17ad3e9..6c0a852f34 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -15,6 +15,7 @@ import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-e import { NoteReaction } from '../../../models/entities/note-reaction'; export default async (user: User, note: Note, reaction?: string) => { + // TODO: cache reaction = await toDbReaction(reaction, user.host); let record: NoteReaction; -- cgit v1.2.3-freya From 87c8f9ff953499340496e9c5db09c93eaff08851 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 19 Mar 2021 20:43:24 +0900 Subject: perf: Reduce database query --- src/client/components/note-detailed.vue | 10 ++- src/client/components/note.vue | 10 ++- src/client/ui/chat/note.vue | 10 ++- src/server/api/endpoints/notes/mentions.ts | 6 +- src/server/api/stream/index.ts | 38 ++++++++--- src/services/note/read-mention.ts | 29 ++++++++ src/services/note/read-specified-note.ts | 29 ++++++++ src/services/note/read.ts | 105 ----------------------------- 8 files changed, 114 insertions(+), 123 deletions(-) create mode 100644 src/services/note/read-mention.ts create mode 100644 src/services/note/read-specified-note.ts delete mode 100644 src/services/note/read.ts (limited to 'src/services/note') diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index 1ef3f43389..4ad3d2d898 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -350,7 +350,15 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); + this.connection.send('sn', { id: this.appearNote.id }); + if (this.appearNote.userId !== this.$i.id) { + if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) { + this.connection.send('readMention', { id: this.appearNote.id }); + } + if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) { + this.connection.send('readSpecifiedNote', { id: this.appearNote.id }); + } + } if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 65e09b7802..3b59afd71d 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -325,7 +325,15 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); + this.connection.send('sn', { id: this.appearNote.id }); + if (this.appearNote.userId !== this.$i.id) { + if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) { + this.connection.send('readMention', { id: this.appearNote.id }); + } + if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) { + this.connection.send('readSpecifiedNote', { id: this.appearNote.id }); + } + } if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue index 5a4a13d889..29bc61d9c5 100644 --- a/src/client/ui/chat/note.vue +++ b/src/client/ui/chat/note.vue @@ -325,7 +325,15 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); + this.connection.send('sn', { id: this.appearNote.id }); + if (this.appearNote.userId !== this.$i.id) { + if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) { + this.connection.send('readMention', { id: this.appearNote.id }); + } + if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) { + this.connection.send('readSpecifiedNote', { id: this.appearNote.id }); + } + } if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 8a9d295d38..1e3014bd46 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -1,12 +1,12 @@ import $ from 'cafy'; import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import read from '../../../../services/note/read'; import { Notes, Followings } from '../../../../models'; import { generateVisibilityQuery } from '../../common/generate-visibility-query'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { Brackets } from 'typeorm'; +import { readMention } from '../../../../services/note/read-mention'; export const meta = { desc: { @@ -79,9 +79,7 @@ export default define(meta, async (ps, user) => { const mentions = await query.take(ps.limit!).getMany(); - for (const note of mentions) { - read(user.id, note.id); - } + readMention(user.id, mentions.map(n => n.id)); return await Notes.packMany(mentions, user); }); diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index bb37cfa622..4a87f61e7f 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -2,7 +2,6 @@ import autobind from 'autobind-decorator'; import * as websocket from 'websocket'; import { readNotification } from '../common/read-notification'; import call from '../call'; -import readNote from '../../../services/note/read'; import Channel from './channel'; import channels from './channels'; import { EventEmitter } from 'events'; @@ -14,6 +13,8 @@ import { AccessToken } from '../../../models/entities/access-token'; import { UserProfile } from '../../../models/entities/user-profile'; import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '../../../services/stream'; import { UserGroup } from '../../../models/entities/user-group'; +import { readMention } from '../../../services/note/read-mention'; +import { readSpecifiedNote } from '../../../services/note/read-specified-note'; /** * Main stream connection @@ -86,9 +87,10 @@ export default class Connection { switch (type) { case 'api': this.onApiRequest(body); break; case 'readNotification': this.onReadNotification(body); break; - case 'subNote': this.onSubscribeNote(body, true); break; - case 'sn': this.onSubscribeNote(body, true); break; // alias - case 's': this.onSubscribeNote(body, false); break; + case 'readMention': this.onReadMention(body); break; + case 'readSpecifiedNote': this.onReadSpecifiedNote(body); break; + case 'subNote': this.onSubscribeNote(body); break; + case 'sn': this.onSubscribeNote(body); break; // alias case 'unsubNote': this.onUnsubscribeNote(body); break; case 'un': this.onUnsubscribeNote(body); break; // alias case 'connect': this.onChannelConnectRequested(body); break; @@ -141,11 +143,31 @@ export default class Connection { readNotification(this.user!.id, [payload.id]); } + @autobind + private onReadMention(payload: any) { + if (!payload.id) return; + if (this.user) { + // TODO: ある程度まとめてreadMentionするようにする + // 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadMentionに渡すような実装にする + readMention(this.user.id, [payload.id]); + } + } + + @autobind + private onReadSpecifiedNote(payload: any) { + if (!payload.id) return; + if (this.user) { + // TODO: ある程度まとめてreadSpecifiedNoteするようにする + // 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadSpecifiedNoteに渡すような実装にする + readSpecifiedNote(this.user.id, [payload.id]); + } + } + /** * 投稿購読要求時 */ @autobind - private onSubscribeNote(payload: any, read: boolean) { + private onSubscribeNote(payload: any) { if (!payload.id) return; if (this.subscribingNotes[payload.id] == null) { @@ -157,12 +179,6 @@ export default class Connection { if (this.subscribingNotes[payload.id] === 1) { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } - - if (this.user && read) { - // TODO: クライアントでタイムライン読み込みなどすると、一度に大量のreadNoteが発生しクエリ数がすごいことになるので、ある程度まとめてreadNoteするようにする - // 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadNoteに渡すような実装にする - readNote(this.user.id, payload.id); - } } /** diff --git a/src/services/note/read-mention.ts b/src/services/note/read-mention.ts new file mode 100644 index 0000000000..2a668ecd6c --- /dev/null +++ b/src/services/note/read-mention.ts @@ -0,0 +1,29 @@ +import { publishMainStream } from '../stream'; +import { Note } from '../../models/entities/note'; +import { User } from '../../models/entities/user'; +import { NoteUnreads } from '../../models'; +import { In } from 'typeorm'; + +/** + * Mark a mention note as read + */ +export async function readMention( + userId: User['id'], + noteIds: Note['id'][] +) { + // Remove the records + await NoteUnreads.delete({ + userId: userId, + noteId: In(noteIds), + }); + + const mentionsCount = await NoteUnreads.count({ + userId: userId, + isMentioned: true + }); + + if (mentionsCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadMentions'); + } +} diff --git a/src/services/note/read-specified-note.ts b/src/services/note/read-specified-note.ts new file mode 100644 index 0000000000..0fcb66bf98 --- /dev/null +++ b/src/services/note/read-specified-note.ts @@ -0,0 +1,29 @@ +import { publishMainStream } from '../stream'; +import { Note } from '../../models/entities/note'; +import { User } from '../../models/entities/user'; +import { NoteUnreads } from '../../models'; +import { In } from 'typeorm'; + +/** + * Mark a specified note as read + */ +export async function readSpecifiedNote( + userId: User['id'], + noteIds: Note['id'][] +) { + // Remove the records + await NoteUnreads.delete({ + userId: userId, + noteId: In(noteIds), + }); + + const specifiedCount = await NoteUnreads.count({ + userId: userId, + isSpecified: true + }); + + if (specifiedCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } +} diff --git a/src/services/note/read.ts b/src/services/note/read.ts deleted file mode 100644 index 5a39ab30b7..0000000000 --- a/src/services/note/read.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { publishMainStream } from '../stream'; -import { Note } from '../../models/entities/note'; -import { User } from '../../models/entities/user'; -import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models'; -import { Not, IsNull } from 'typeorm'; - -/** - * Mark a note as read - */ -export default async function( - userId: User['id'], - noteId: Note['id'] -) { - async function careNoteUnreads() { - const exist = await NoteUnreads.findOne({ - userId: userId, - noteId: noteId, - }); - - if (!exist) return; - - // Remove the record - await NoteUnreads.delete({ - userId: userId, - noteId: noteId, - }); - - if (exist.isMentioned) { - NoteUnreads.count({ - userId: userId, - isMentioned: true - }).then(mentionsCount => { - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadMentions'); - } - }); - } - - if (exist.isSpecified) { - NoteUnreads.count({ - userId: userId, - isSpecified: true - }).then(specifiedCount => { - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); - } - }); - } - - if (exist.noteChannelId) { - NoteUnreads.count({ - userId: userId, - noteChannelId: Not(IsNull()) - }).then(channelNoteCount => { - if (channelNoteCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllChannels'); - } - }); - } - } - - async function careAntenna() { - const beforeUnread = await Users.getHasUnreadAntenna(userId); - if (!beforeUnread) return; - - const antennas = await Antennas.find({ userId }); - - await Promise.all(antennas.map(async antenna => { - const countBefore = await AntennaNotes.count({ - antennaId: antenna.id, - read: false - }); - - if (countBefore === 0) return; - - await AntennaNotes.update({ - antennaId: antenna.id, - noteId: noteId - }, { - read: true - }); - - const countAfter = await AntennaNotes.count({ - antennaId: antenna.id, - read: false - }); - - if (countAfter === 0) { - publishMainStream(userId, 'readAntenna', antenna); - } - })); - - Users.getHasUnreadAntenna(userId).then(unread => { - if (!unread) { - publishMainStream(userId, 'readAllAntennas'); - } - }); - } - - careNoteUnreads(); - careAntenna(); -} -- cgit v1.2.3-freya From 630464f38d2524ddc5d11d2abd4fddcccc4240d4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 21 Mar 2021 15:35:02 +0900 Subject: Revert "perf: Reduce database query" This reverts commit 87c8f9ff953499340496e9c5db09c93eaff08851. --- src/client/components/note-detailed.vue | 10 +-- src/client/components/note.vue | 10 +-- src/client/ui/chat/note.vue | 10 +-- src/server/api/endpoints/notes/mentions.ts | 6 +- src/server/api/stream/index.ts | 38 +++-------- src/services/note/read-mention.ts | 29 -------- src/services/note/read-specified-note.ts | 29 -------- src/services/note/read.ts | 105 +++++++++++++++++++++++++++++ 8 files changed, 123 insertions(+), 114 deletions(-) delete mode 100644 src/services/note/read-mention.ts delete mode 100644 src/services/note/read-specified-note.ts create mode 100644 src/services/note/read.ts (limited to 'src/services/note') diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index 4ad3d2d898..1ef3f43389 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -350,15 +350,7 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send('sn', { id: this.appearNote.id }); - if (this.appearNote.userId !== this.$i.id) { - if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) { - this.connection.send('readMention', { id: this.appearNote.id }); - } - if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) { - this.connection.send('readSpecifiedNote', { id: this.appearNote.id }); - } - } + this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 3b59afd71d..65e09b7802 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -325,15 +325,7 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send('sn', { id: this.appearNote.id }); - if (this.appearNote.userId !== this.$i.id) { - if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) { - this.connection.send('readMention', { id: this.appearNote.id }); - } - if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) { - this.connection.send('readSpecifiedNote', { id: this.appearNote.id }); - } - } + this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue index 29bc61d9c5..5a4a13d889 100644 --- a/src/client/ui/chat/note.vue +++ b/src/client/ui/chat/note.vue @@ -325,15 +325,7 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send('sn', { id: this.appearNote.id }); - if (this.appearNote.userId !== this.$i.id) { - if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) { - this.connection.send('readMention', { id: this.appearNote.id }); - } - if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) { - this.connection.send('readSpecifiedNote', { id: this.appearNote.id }); - } - } + this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 56640ec1ab..30844774e0 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -1,12 +1,12 @@ import $ from 'cafy'; import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import read from '../../../../services/note/read'; import { Notes, Followings } from '../../../../models'; import { generateVisibilityQuery } from '../../common/generate-visibility-query'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { Brackets } from 'typeorm'; -import { readMention } from '../../../../services/note/read-mention'; export const meta = { desc: { @@ -83,7 +83,9 @@ export default define(meta, async (ps, user) => { const mentions = await query.take(ps.limit!).getMany(); - readMention(user.id, mentions.map(n => n.id)); + for (const note of mentions) { + read(user.id, note.id); + } return await Notes.packMany(mentions, user); }); diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index 748e894f83..f67faee1ce 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -2,6 +2,7 @@ import autobind from 'autobind-decorator'; import * as websocket from 'websocket'; import { readNotification } from '../common/read-notification'; import call from '../call'; +import readNote from '../../../services/note/read'; import Channel from './channel'; import channels from './channels'; import { EventEmitter } from 'events'; @@ -13,8 +14,6 @@ import { AccessToken } from '../../../models/entities/access-token'; import { UserProfile } from '../../../models/entities/user-profile'; import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '../../../services/stream'; import { UserGroup } from '../../../models/entities/user-group'; -import { readMention } from '../../../services/note/read-mention'; -import { readSpecifiedNote } from '../../../services/note/read-specified-note'; /** * Main stream connection @@ -116,10 +115,9 @@ export default class Connection { switch (type) { case 'api': this.onApiRequest(body); break; case 'readNotification': this.onReadNotification(body); break; - case 'readMention': this.onReadMention(body); break; - case 'readSpecifiedNote': this.onReadSpecifiedNote(body); break; - case 'subNote': this.onSubscribeNote(body); break; - case 'sn': this.onSubscribeNote(body); break; // alias + case 'subNote': this.onSubscribeNote(body, true); break; + case 'sn': this.onSubscribeNote(body, true); break; // alias + case 's': this.onSubscribeNote(body, false); break; case 'unsubNote': this.onUnsubscribeNote(body); break; case 'un': this.onUnsubscribeNote(body); break; // alias case 'connect': this.onChannelConnectRequested(body); break; @@ -172,31 +170,11 @@ export default class Connection { readNotification(this.user!.id, [payload.id]); } - @autobind - private onReadMention(payload: any) { - if (!payload.id) return; - if (this.user) { - // TODO: ある程度まとめてreadMentionするようにする - // 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadMentionに渡すような実装にする - readMention(this.user.id, [payload.id]); - } - } - - @autobind - private onReadSpecifiedNote(payload: any) { - if (!payload.id) return; - if (this.user) { - // TODO: ある程度まとめてreadSpecifiedNoteするようにする - // 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadSpecifiedNoteに渡すような実装にする - readSpecifiedNote(this.user.id, [payload.id]); - } - } - /** * 投稿購読要求時 */ @autobind - private onSubscribeNote(payload: any) { + private onSubscribeNote(payload: any, read: boolean) { if (!payload.id) return; if (this.subscribingNotes[payload.id] == null) { @@ -208,6 +186,12 @@ export default class Connection { if (this.subscribingNotes[payload.id] === 1) { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } + + if (this.user && read) { + // TODO: クライアントでタイムライン読み込みなどすると、一度に大量のreadNoteが発生しクエリ数がすごいことになるので、ある程度まとめてreadNoteするようにする + // 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadNoteに渡すような実装にする + readNote(this.user.id, payload.id); + } } /** diff --git a/src/services/note/read-mention.ts b/src/services/note/read-mention.ts deleted file mode 100644 index 2a668ecd6c..0000000000 --- a/src/services/note/read-mention.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { publishMainStream } from '../stream'; -import { Note } from '../../models/entities/note'; -import { User } from '../../models/entities/user'; -import { NoteUnreads } from '../../models'; -import { In } from 'typeorm'; - -/** - * Mark a mention note as read - */ -export async function readMention( - userId: User['id'], - noteIds: Note['id'][] -) { - // Remove the records - await NoteUnreads.delete({ - userId: userId, - noteId: In(noteIds), - }); - - const mentionsCount = await NoteUnreads.count({ - userId: userId, - isMentioned: true - }); - - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadMentions'); - } -} diff --git a/src/services/note/read-specified-note.ts b/src/services/note/read-specified-note.ts deleted file mode 100644 index 0fcb66bf98..0000000000 --- a/src/services/note/read-specified-note.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { publishMainStream } from '../stream'; -import { Note } from '../../models/entities/note'; -import { User } from '../../models/entities/user'; -import { NoteUnreads } from '../../models'; -import { In } from 'typeorm'; - -/** - * Mark a specified note as read - */ -export async function readSpecifiedNote( - userId: User['id'], - noteIds: Note['id'][] -) { - // Remove the records - await NoteUnreads.delete({ - userId: userId, - noteId: In(noteIds), - }); - - const specifiedCount = await NoteUnreads.count({ - userId: userId, - isSpecified: true - }); - - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); - } -} diff --git a/src/services/note/read.ts b/src/services/note/read.ts new file mode 100644 index 0000000000..5a39ab30b7 --- /dev/null +++ b/src/services/note/read.ts @@ -0,0 +1,105 @@ +import { publishMainStream } from '../stream'; +import { Note } from '../../models/entities/note'; +import { User } from '../../models/entities/user'; +import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models'; +import { Not, IsNull } from 'typeorm'; + +/** + * Mark a note as read + */ +export default async function( + userId: User['id'], + noteId: Note['id'] +) { + async function careNoteUnreads() { + const exist = await NoteUnreads.findOne({ + userId: userId, + noteId: noteId, + }); + + if (!exist) return; + + // Remove the record + await NoteUnreads.delete({ + userId: userId, + noteId: noteId, + }); + + if (exist.isMentioned) { + NoteUnreads.count({ + userId: userId, + isMentioned: true + }).then(mentionsCount => { + if (mentionsCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadMentions'); + } + }); + } + + if (exist.isSpecified) { + NoteUnreads.count({ + userId: userId, + isSpecified: true + }).then(specifiedCount => { + if (specifiedCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } + }); + } + + if (exist.noteChannelId) { + NoteUnreads.count({ + userId: userId, + noteChannelId: Not(IsNull()) + }).then(channelNoteCount => { + if (channelNoteCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllChannels'); + } + }); + } + } + + async function careAntenna() { + const beforeUnread = await Users.getHasUnreadAntenna(userId); + if (!beforeUnread) return; + + const antennas = await Antennas.find({ userId }); + + await Promise.all(antennas.map(async antenna => { + const countBefore = await AntennaNotes.count({ + antennaId: antenna.id, + read: false + }); + + if (countBefore === 0) return; + + await AntennaNotes.update({ + antennaId: antenna.id, + noteId: noteId + }, { + read: true + }); + + const countAfter = await AntennaNotes.count({ + antennaId: antenna.id, + read: false + }); + + if (countAfter === 0) { + publishMainStream(userId, 'readAntenna', antenna); + } + })); + + Users.getHasUnreadAntenna(userId).then(unread => { + if (!unread) { + publishMainStream(userId, 'readAllAntennas'); + } + }); + } + + careNoteUnreads(); + careAntenna(); +} -- cgit v1.2.3-freya From 667d58bad4544d6e9dc75cfc4e6216179e2bc1aa Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 21 Mar 2021 17:38:09 +0900 Subject: better note read handling --- src/client/components/note-detailed.vue | 3 +- src/client/components/note.vue | 3 +- src/client/ui/chat/note.vue | 3 +- src/daemons/janitor.ts | 2 + src/server/api/endpoints/notes/mentions.ts | 4 +- src/server/api/stream/channels/antenna.ts | 2 + src/server/api/stream/channels/channel.ts | 2 + src/server/api/stream/channels/global-timeline.ts | 2 + src/server/api/stream/channels/hashtag.ts | 2 + src/server/api/stream/channels/home-timeline.ts | 2 + src/server/api/stream/channels/hybrid-timeline.ts | 2 + src/server/api/stream/channels/local-timeline.ts | 2 + src/server/api/stream/channels/main.ts | 8 ++- src/server/api/stream/index.ts | 58 +++++++++++++--- src/services/note/read.ts | 80 +++++++++-------------- 15 files changed, 109 insertions(+), 66 deletions(-) (limited to 'src/services/note') diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index 1ef3f43389..ea26d31100 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -350,7 +350,8 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); + // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する + this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id }); if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 65e09b7802..70f49fef7e 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -325,7 +325,8 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); + // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する + this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id }); if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue index 5a4a13d889..97275875ca 100644 --- a/src/client/ui/chat/note.vue +++ b/src/client/ui/chat/note.vue @@ -325,7 +325,8 @@ export default defineComponent({ capture(withHandler = false) { if (this.$i) { - this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); + // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する + this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id }); if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); } }, diff --git a/src/daemons/janitor.ts b/src/daemons/janitor.ts index 462ebf915c..c079086427 100644 --- a/src/daemons/janitor.ts +++ b/src/daemons/janitor.ts @@ -1,3 +1,5 @@ +// TODO: 消したい + const interval = 30 * 60 * 1000; import { AttestationChallenges } from '../models'; import { LessThan } from 'typeorm'; diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 30844774e0..30368ea578 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -83,9 +83,7 @@ export default define(meta, async (ps, user) => { const mentions = await query.take(ps.limit!).getMany(); - for (const note of mentions) { - read(user.id, note.id); - } + read(user.id, mentions.map(note => note.id)); return await Notes.packMany(mentions, user); }); diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts index b5a792f814..36a474f2ac 100644 --- a/src/server/api/stream/channels/antenna.ts +++ b/src/server/api/stream/channels/antenna.ts @@ -27,6 +27,8 @@ export default class extends Channel { // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isMutedUserRelated(note, this.muting)) return; + this.connection.cacheNote(note); + this.send('note', note); } else { this.send(type, body); diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts index aa570d1ef4..47a52465b2 100644 --- a/src/server/api/stream/channels/channel.ts +++ b/src/server/api/stream/channels/channel.ts @@ -43,6 +43,8 @@ export default class extends Channel { // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isMutedUserRelated(note, this.muting)) return; + this.connection.cacheNote(note); + this.send('note', note); } diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts index 8c97e67226..8353f45323 100644 --- a/src/server/api/stream/channels/global-timeline.ts +++ b/src/server/api/stream/channels/global-timeline.ts @@ -56,6 +56,8 @@ export default class extends Channel { // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; + this.connection.cacheNote(note); + this.send('note', note); } diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts index 41447039d5..1b7f8efcc1 100644 --- a/src/server/api/stream/channels/hashtag.ts +++ b/src/server/api/stream/channels/hashtag.ts @@ -37,6 +37,8 @@ export default class extends Channel { // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isMutedUserRelated(note, this.muting)) return; + this.connection.cacheNote(note); + this.send('note', note); } diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts index 6cfa6eae7b..59ba31c316 100644 --- a/src/server/api/stream/channels/home-timeline.ts +++ b/src/server/api/stream/channels/home-timeline.ts @@ -64,6 +64,8 @@ export default class extends Channel { // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; + this.connection.cacheNote(note); + this.send('note', note); } diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts index a9e577cacb..9715e9973f 100644 --- a/src/server/api/stream/channels/hybrid-timeline.ts +++ b/src/server/api/stream/channels/hybrid-timeline.ts @@ -73,6 +73,8 @@ export default class extends Channel { // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; + this.connection.cacheNote(note); + this.send('note', note); } diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts index a3a5e491fc..e159c72d60 100644 --- a/src/server/api/stream/channels/local-timeline.ts +++ b/src/server/api/stream/channels/local-timeline.ts @@ -58,6 +58,8 @@ export default class extends Channel { // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; + this.connection.cacheNote(note); + this.send('note', note); } diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts index b69c2ec355..780bc0b89f 100644 --- a/src/server/api/stream/channels/main.ts +++ b/src/server/api/stream/channels/main.ts @@ -18,18 +18,22 @@ export default class extends Channel { case 'notification': { if (this.muting.has(body.userId)) return; if (body.note && body.note.isHidden) { - body.note = await Notes.pack(body.note.id, this.user, { + const note = await Notes.pack(body.note.id, this.user, { detail: true }); + this.connection.cacheNote(note); + body.note = note; } break; } case 'mention': { if (this.muting.has(body.userId)) return; if (body.isHidden) { - body = await Notes.pack(body.id, this.user, { + const note = await Notes.pack(body.id, this.user, { detail: true }); + this.connection.cacheNote(note); + body = note; } break; } diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index f67faee1ce..99ae558696 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -14,6 +14,7 @@ import { AccessToken } from '../../../models/entities/access-token'; import { UserProfile } from '../../../models/entities/user-profile'; import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '../../../services/stream'; import { UserGroup } from '../../../models/entities/user-group'; +import { PackedNote } from '../../../models/repositories/note'; /** * Main stream connection @@ -29,6 +30,7 @@ export default class Connection { public subscriber: EventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; + private cachedNotes: PackedNote[] = []; constructor( wsConnection: websocket.connection, @@ -115,9 +117,9 @@ export default class Connection { switch (type) { case 'api': this.onApiRequest(body); break; case 'readNotification': this.onReadNotification(body); break; - case 'subNote': this.onSubscribeNote(body, true); break; - case 'sn': this.onSubscribeNote(body, true); break; // alias - case 's': this.onSubscribeNote(body, false); break; + case 'subNote': this.onSubscribeNote(body); break; + case 's': this.onSubscribeNote(body); break; // alias + case 'sr': this.onSubscribeNote(body); this.readNote(body); break; case 'unsubNote': this.onUnsubscribeNote(body); break; case 'un': this.onUnsubscribeNote(body); break; // alias case 'connect': this.onChannelConnectRequested(body); break; @@ -138,6 +140,48 @@ export default class Connection { this.sendMessageToWs(type, body); } + @autobind + public cacheNote(note: PackedNote) { + const add = (note: PackedNote) => { + const existIndex = this.cachedNotes.findIndex(n => n.id === note.id); + if (existIndex > -1) { + this.cachedNotes[existIndex] = note; + return; + } + + this.cachedNotes.unshift(note); + if (this.cachedNotes.length > 32) { + this.cachedNotes.splice(32); + } + }; + + add(note); + if (note.reply) add(note.reply); + if (note.renote) add(note.renote); + } + + @autobind + private readNote(body: any) { + const id = body.id; + + const note = this.cachedNotes.find(n => n.id === id); + if (note == null) return; + + if (this.user && (note.userId !== this.user.id)) { + if (note.mentions && note.mentions.includes(this.user.id)) { + readNote(this.user.id, [note]); + } else if (note.visibleUserIds && note.visibleUserIds.includes(this.user.id)) { + readNote(this.user.id, [note]); + } + + if (this.followingChannels.has(note.channelId)) { + // TODO + } + + // TODO: アンテナの既読処理 + } + } + /** * APIリクエスト要求時 */ @@ -174,7 +218,7 @@ export default class Connection { * 投稿購読要求時 */ @autobind - private onSubscribeNote(payload: any, read: boolean) { + private onSubscribeNote(payload: any) { if (!payload.id) return; if (this.subscribingNotes[payload.id] == null) { @@ -186,12 +230,6 @@ export default class Connection { if (this.subscribingNotes[payload.id] === 1) { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } - - if (this.user && read) { - // TODO: クライアントでタイムライン読み込みなどすると、一度に大量のreadNoteが発生しクエリ数がすごいことになるので、ある程度まとめてreadNoteするようにする - // 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadNoteに渡すような実装にする - readNote(this.user.id, payload.id); - } } /** diff --git a/src/services/note/read.ts b/src/services/note/read.ts index 5a39ab30b7..35279db411 100644 --- a/src/services/note/read.ts +++ b/src/services/note/read.ts @@ -2,70 +2,54 @@ import { publishMainStream } from '../stream'; import { Note } from '../../models/entities/note'; import { User } from '../../models/entities/user'; import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models'; -import { Not, IsNull } from 'typeorm'; +import { Not, IsNull, In } from 'typeorm'; /** - * Mark a note as read + * Mark notes as read */ export default async function( userId: User['id'], - noteId: Note['id'] + noteIds: Note['id'][] ) { async function careNoteUnreads() { - const exist = await NoteUnreads.findOne({ - userId: userId, - noteId: noteId, - }); - - if (!exist) return; - // Remove the record await NoteUnreads.delete({ userId: userId, - noteId: noteId, + noteId: In(noteIds), }); - if (exist.isMentioned) { - NoteUnreads.count({ - userId: userId, - isMentioned: true - }).then(mentionsCount => { - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadMentions'); - } - }); - } + NoteUnreads.count({ + userId: userId, + isMentioned: true + }).then(mentionsCount => { + if (mentionsCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadMentions'); + } + }); - if (exist.isSpecified) { - NoteUnreads.count({ - userId: userId, - isSpecified: true - }).then(specifiedCount => { - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); - } - }); - } + NoteUnreads.count({ + userId: userId, + isSpecified: true + }).then(specifiedCount => { + if (specifiedCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } + }); - if (exist.noteChannelId) { - NoteUnreads.count({ - userId: userId, - noteChannelId: Not(IsNull()) - }).then(channelNoteCount => { - if (channelNoteCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllChannels'); - } - }); - } + NoteUnreads.count({ + userId: userId, + noteChannelId: Not(IsNull()) + }).then(channelNoteCount => { + if (channelNoteCount === 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllChannels'); + } + }); } async function careAntenna() { - const beforeUnread = await Users.getHasUnreadAntenna(userId); - if (!beforeUnread) return; - const antennas = await Antennas.find({ userId }); await Promise.all(antennas.map(async antenna => { @@ -78,7 +62,7 @@ export default async function( await AntennaNotes.update({ antennaId: antenna.id, - noteId: noteId + noteId: In(noteIds) }, { read: true }); -- cgit v1.2.3-freya From c4c20bee7c58ea7330dbc890b9564bd100ac6e25 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 21 Mar 2021 21:27:09 +0900 Subject: wip #6441 --- src/models/entities/note-reaction.ts | 4 ++-- src/server/api/endpoints/admin/invite.ts | 2 +- src/server/api/endpoints/admin/promo/create.ts | 2 +- src/server/api/endpoints/auth/accept.ts | 2 +- src/server/api/endpoints/channels/follow.ts | 2 +- src/server/api/endpoints/clips/add-note.ts | 2 +- src/server/api/endpoints/i/read-announcement.ts | 2 +- src/server/api/endpoints/miauth/gen-token.ts | 2 +- src/server/api/endpoints/notes/favorites/create.ts | 2 +- src/server/api/endpoints/pages/like.ts | 2 +- src/server/api/endpoints/promo/read.ts | 2 +- src/server/api/endpoints/sw/register.ts | 2 +- src/server/api/endpoints/users/groups/create.ts | 2 +- .../api/endpoints/users/groups/invitations/accept.ts | 2 +- src/server/api/private/signin.ts | 4 ++-- src/services/add-note-to-antenna.ts | 2 +- src/services/blocking/create.ts | 2 +- src/services/following/create.ts | 2 +- src/services/i/pin.ts | 2 +- src/services/insert-moderation-log.ts | 2 +- src/services/messages/create.ts | 6 ++++-- src/services/note/create.ts | 2 +- src/services/note/polls/vote.ts | 2 +- src/services/note/reaction/create.ts | 16 ++++++++-------- src/services/note/unread.ts | 6 ++++-- src/services/note/watch.ts | 2 +- src/services/update-hashtag.ts | 4 ++-- src/services/user-list/push.ts | 2 +- 28 files changed, 44 insertions(+), 40 deletions(-) (limited to 'src/services/note') diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts index 69bb663fd3..674dc3639e 100644 --- a/src/models/entities/note-reaction.ts +++ b/src/models/entities/note-reaction.ts @@ -23,7 +23,7 @@ export class NoteReaction { onDelete: 'CASCADE' }) @JoinColumn() - public user: User | null; + public user?: User | null; @Index() @Column(id()) @@ -33,7 +33,7 @@ export class NoteReaction { onDelete: 'CASCADE' }) @JoinColumn() - public note: Note | null; + public note?: Note | null; // TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため) diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts index 4529d16adf..987105791f 100644 --- a/src/server/api/endpoints/admin/invite.ts +++ b/src/server/api/endpoints/admin/invite.ts @@ -38,7 +38,7 @@ export default define(meta, async () => { chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns) }); - await RegistrationTickets.save({ + await RegistrationTickets.insert({ id: genId(), createdAt: new Date(), code, diff --git a/src/server/api/endpoints/admin/promo/create.ts b/src/server/api/endpoints/admin/promo/create.ts index 8b96d563c2..aa22e68ebd 100644 --- a/src/server/api/endpoints/admin/promo/create.ts +++ b/src/server/api/endpoints/admin/promo/create.ts @@ -53,7 +53,7 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.alreadyPromoted); } - await PromoNotes.save({ + await PromoNotes.insert({ noteId: note.id, createdAt: new Date(), expiresAt: new Date(ps.expiresAt), diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts index 6d4d31fa1e..444053a946 100644 --- a/src/server/api/endpoints/auth/accept.ts +++ b/src/server/api/endpoints/auth/accept.ts @@ -58,7 +58,7 @@ export default define(meta, async (ps, user) => { const now = new Date(); // Insert access token doc - await AccessTokens.save({ + await AccessTokens.insert({ id: genId(), createdAt: now, lastUsedAt: now, diff --git a/src/server/api/endpoints/channels/follow.ts b/src/server/api/endpoints/channels/follow.ts index 11c6e37ff7..c5976a8a34 100644 --- a/src/server/api/endpoints/channels/follow.ts +++ b/src/server/api/endpoints/channels/follow.ts @@ -37,7 +37,7 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.noSuchChannel); } - await ChannelFollowings.save({ + await ChannelFollowings.insert({ id: genId(), createdAt: new Date(), followerId: user.id, diff --git a/src/server/api/endpoints/clips/add-note.ts b/src/server/api/endpoints/clips/add-note.ts index 4f5cc649e3..ee6a117b2d 100644 --- a/src/server/api/endpoints/clips/add-note.ts +++ b/src/server/api/endpoints/clips/add-note.ts @@ -68,7 +68,7 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.alreadyClipped); } - await ClipNotes.save({ + await ClipNotes.insert({ id: genId(), noteId: note.id, clipId: clip.id diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts index 4a4a021af9..d6acb3d2e6 100644 --- a/src/server/api/endpoints/i/read-announcement.ts +++ b/src/server/api/endpoints/i/read-announcement.ts @@ -52,7 +52,7 @@ export default define(meta, async (ps, user) => { } // Create read - await AnnouncementReads.save({ + await AnnouncementReads.insert({ id: genId(), createdAt: new Date(), announcementId: ps.announcementId, diff --git a/src/server/api/endpoints/miauth/gen-token.ts b/src/server/api/endpoints/miauth/gen-token.ts index 0634debb1e..401ed16389 100644 --- a/src/server/api/endpoints/miauth/gen-token.ts +++ b/src/server/api/endpoints/miauth/gen-token.ts @@ -52,7 +52,7 @@ export default define(meta, async (ps, user) => { const now = new Date(); // Insert access token doc - await AccessTokens.save({ + await AccessTokens.insert({ id: genId(), createdAt: now, lastUsedAt: now, diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index 952bbfd0eb..d66ce37a46 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -61,7 +61,7 @@ export default define(meta, async (ps, user) => { } // Create favorite - await NoteFavorites.save({ + await NoteFavorites.insert({ id: genId(), createdAt: new Date(), noteId: note.id, diff --git a/src/server/api/endpoints/pages/like.ts b/src/server/api/endpoints/pages/like.ts index 5c7e13f1c8..3fc2b6ca23 100644 --- a/src/server/api/endpoints/pages/like.ts +++ b/src/server/api/endpoints/pages/like.ts @@ -68,7 +68,7 @@ export default define(meta, async (ps, user) => { } // Create like - await PageLikes.save({ + await PageLikes.insert({ id: genId(), createdAt: new Date(), pageId: page.id, diff --git a/src/server/api/endpoints/promo/read.ts b/src/server/api/endpoints/promo/read.ts index 57eb0681e5..63c90e5d7f 100644 --- a/src/server/api/endpoints/promo/read.ts +++ b/src/server/api/endpoints/promo/read.ts @@ -46,7 +46,7 @@ export default define(meta, async (ps, user) => { return; } - await PromoReads.save({ + await PromoReads.insert({ id: genId(), createdAt: new Date(), noteId: note.id, diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index ceb70a9274..9fc70b5609 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -58,7 +58,7 @@ export default define(meta, async (ps, user) => { }; } - await SwSubscriptions.save({ + await SwSubscriptions.insert({ id: genId(), createdAt: new Date(), userId: user.id, diff --git a/src/server/api/endpoints/users/groups/create.ts b/src/server/api/endpoints/users/groups/create.ts index ca011d5cd6..78d2714874 100644 --- a/src/server/api/endpoints/users/groups/create.ts +++ b/src/server/api/endpoints/users/groups/create.ts @@ -39,7 +39,7 @@ export default define(meta, async (ps, user) => { } as UserGroup); // Push the owner - await UserGroupJoinings.save({ + await UserGroupJoinings.insert({ id: genId(), createdAt: new Date(), userId: user.id, diff --git a/src/server/api/endpoints/users/groups/invitations/accept.ts b/src/server/api/endpoints/users/groups/invitations/accept.ts index e86709f83b..2fa22bcf7e 100644 --- a/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -52,7 +52,7 @@ export default define(meta, async (ps, user) => { } // Push the user - await UserGroupJoinings.save({ + await UserGroupJoinings.insert({ id: genId(), createdAt: new Date(), userId: user.id, diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 7a5efc6cc9..d8f2e6d516 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -53,7 +53,7 @@ export default async (ctx: Koa.Context) => { async function fail(status?: number, failure?: { error: string }) { // Append signin history - await Signins.save({ + await Signins.insert({ id: genId(), createdAt: new Date(), userId: user.id, @@ -198,7 +198,7 @@ export default async (ctx: Koa.Context) => { const challengeId = genId(); - await AttestationChallenges.save({ + await AttestationChallenges.insert({ userId: user.id, id: challengeId, challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'), diff --git a/src/services/add-note-to-antenna.ts b/src/services/add-note-to-antenna.ts index 2c893488c3..3ba3d1eef5 100644 --- a/src/services/add-note-to-antenna.ts +++ b/src/services/add-note-to-antenna.ts @@ -10,7 +10,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: U // 通知しない設定になっているか、自分自身の投稿なら既読にする const read = !antenna.notify || (antenna.userId === noteUser.id); - AntennaNotes.save({ + AntennaNotes.insert({ id: genId(), antennaId: antenna.id, noteId: note.id, diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts index 4f0238db91..dec48d26de 100644 --- a/src/services/blocking/create.ts +++ b/src/services/blocking/create.ts @@ -18,7 +18,7 @@ export default async function(blocker: User, blockee: User) { unFollow(blockee, blocker) ]); - await Blockings.save({ + await Blockings.insert({ id: genId(), createdAt: new Date(), blockerId: blocker.id, diff --git a/src/services/following/create.ts b/src/services/following/create.ts index eb6699b0bf..1ce75caca0 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -22,7 +22,7 @@ export async function insertFollowingDoc(followee: User, follower: User) { let alreadyFollowed = false; - await Followings.save({ + await Followings.insert({ id: genId(), createdAt: new Date(), followerId: follower.id, diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts index 1ff5476b40..f727a10fb6 100644 --- a/src/services/i/pin.ts +++ b/src/services/i/pin.ts @@ -37,7 +37,7 @@ export async function addPinned(user: User, noteId: Note['id']) { throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.'); } - await UserNotePinings.save({ + await UserNotePinings.insert({ id: genId(), createdAt: new Date(), userId: user.id, diff --git a/src/services/insert-moderation-log.ts b/src/services/insert-moderation-log.ts index 33dab97259..87587d3bed 100644 --- a/src/services/insert-moderation-log.ts +++ b/src/services/insert-moderation-log.ts @@ -3,7 +3,7 @@ import { ModerationLogs } from '../models'; import { genId } from '../misc/gen-id'; export async function insertModerationLog(moderator: ILocalUser, type: string, info?: Record) { - await ModerationLogs.save({ + await ModerationLogs.insert({ id: genId(), createdAt: new Date(), userId: moderator.id, diff --git a/src/services/messages/create.ts b/src/services/messages/create.ts index 8646ce37fc..413266d029 100644 --- a/src/services/messages/create.ts +++ b/src/services/messages/create.ts @@ -14,7 +14,7 @@ import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; export async function createMessage(user: User, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | undefined, file: DriveFile | null, uri?: string) { - const message = await MessagingMessages.save({ + const message = { id: genId(), createdAt: new Date(), fileId: file ? file.id : null, @@ -25,7 +25,9 @@ export async function createMessage(user: User, recipientUser: User | undefined, isRead: false, reads: [] as any[], uri - } as MessagingMessage); + } as MessagingMessage; + + await MessagingMessages.insert(message); const messageObj = await MessagingMessages.pack(message); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 563eaac758..7c7e8d9a08 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -247,7 +247,7 @@ export default async (user: User, data: Option, silent = false) => new Promise { if (shouldMute) { - MutedNotes.save({ + MutedNotes.insert({ id: genId(), userId: u.userId, noteId: note.id, diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts index bfcaaa09be..b4ce03ab60 100644 --- a/src/services/note/polls/vote.ts +++ b/src/services/note/polls/vote.ts @@ -29,7 +29,7 @@ export default async function(user: User, note: Note, choice: number) { } // Create vote - await PollVotes.save({ + await PollVotes.insert({ id: genId(), createdAt: new Date(), noteId: note.id, diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 6c0a852f34..897c816de8 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -18,17 +18,17 @@ export default async (user: User, note: Note, reaction?: string) => { // TODO: cache reaction = await toDbReaction(reaction, user.host); - let record: NoteReaction; + let record: NoteReaction = { + id: genId(), + createdAt: new Date(), + noteId: note.id, + userId: user.id, + reaction + }; // Create reaction try { - record = await NoteReactions.save({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - reaction - }); + await NoteReactions.insert(record); } catch (e) { if (isDuplicateKeyValueError(e)) { record = await NoteReactions.findOneOrFail({ diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts index 6fd9ee2cfe..8e6fb4abe8 100644 --- a/src/services/note/unread.ts +++ b/src/services/note/unread.ts @@ -17,7 +17,7 @@ export default async function(userId: User['id'], note: Note, params: { if (mute.map(m => m.muteeId).includes(note.userId)) return; //#endregion - const unread = await NoteUnreads.save({ + const unread = { id: genId(), noteId: note.id, userId: userId, @@ -25,7 +25,9 @@ export default async function(userId: User['id'], note: Note, params: { isMentioned: params.isMentioned, noteChannelId: note.channelId, noteUserId: note.userId, - }); + }; + + await NoteUnreads.insert(unread); // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する setTimeout(async () => { diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts index d3c9553696..966b7f0054 100644 --- a/src/services/note/watch.ts +++ b/src/services/note/watch.ts @@ -10,7 +10,7 @@ export default async (me: User['id'], note: Note) => { return; } - await NoteWatchings.save({ + await NoteWatchings.insert({ id: genId(), createdAt: new Date(), noteId: note.id, diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts index 1dcb582791..3e22846731 100644 --- a/src/services/update-hashtag.ts +++ b/src/services/update-hashtag.ts @@ -86,7 +86,7 @@ export async function updateHashtag(user: User, tag: string, isUserAttached = fa } } else { if (isUserAttached) { - Hashtags.save({ + Hashtags.insert({ id: genId(), name: tag, mentionedUserIds: [], @@ -103,7 +103,7 @@ export async function updateHashtag(user: User, tag: string, isUserAttached = fa attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, } as Hashtag); } else { - Hashtags.save({ + Hashtags.insert({ id: genId(), name: tag, mentionedUserIds: [user.id], diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts index e67be4b027..ba54c04475 100644 --- a/src/services/user-list/push.ts +++ b/src/services/user-list/push.ts @@ -8,7 +8,7 @@ import { fetchProxyAccount } from '../../misc/fetch-proxy-account'; import createFollowing from '../following/create'; export async function pushUserToUserList(target: User, list: UserList) { - await UserListJoinings.save({ + await UserListJoinings.insert({ id: genId(), createdAt: new Date(), userId: target.id, -- cgit v1.2.3-freya From a4a9b8707d2d8100e3601679d479dd3b13e73c9a Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 21 Mar 2021 21:27:49 +0900 Subject: perf(server): Reduce database query --- src/services/note/reaction/create.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/services/note') diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 897c816de8..181099cc2d 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -53,12 +53,11 @@ export default async (user: User, note: Note, reaction?: string) => { await Notes.createQueryBuilder().update() .set({ reactions: () => sql, + score: () => '"score" + 1' }) .where('id = :id', { id: note.id }) .execute(); - Notes.increment({ id: note.id }, 'score', 1); - perUserReactionsChart.update(user, note); // カスタム絵文字リアクションだったら絵文字情報も送る -- cgit v1.2.3-freya From fb194b855b7548672d95071adfb5f354e78d3f32 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 21 Mar 2021 22:09:32 +0900 Subject: perf(server): Reduce database query --- src/services/note/create.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'src/services/note') diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 7c7e8d9a08..a85e72c5f9 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -594,10 +594,13 @@ function saveReply(reply: Note, note: Note) { } function incNotesCountOfUser(user: User) { - Users.increment({ id: user.id }, 'notesCount', 1); - Users.update({ id: user.id }, { - updatedAt: new Date() - }); + Users.createQueryBuilder().update() + .set({ + updatedAt: new Date(), + notesCount: () => '"notesCount" + 1' + }) + .where('id = :id', { id: user.id }) + .execute(); } async function extractMentionedUsers(user: User, tokens: ReturnType): Promise { -- cgit v1.2.3-freya From 82de8b7c50767e71f5414481fbb4e5ff7a449593 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 21 Mar 2021 22:15:45 +0900 Subject: perf(server): Reduce database query --- src/services/note/create.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'src/services/note') diff --git a/src/services/note/create.ts b/src/services/note/create.ts index a85e72c5f9..96177e9758 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -444,8 +444,13 @@ async function renderNoteOrRenoteActivity(data: Option, note: Note) { } function incRenoteCount(renote: Note) { - Notes.increment({ id: renote.id }, 'renoteCount', 1); - Notes.increment({ id: renote.id }, 'score', 1); + Notes.createQueryBuilder().update() + .set({ + renoteCount: () => '"renoteCount" + 1', + score: () => '"score" + 1' + }) + .where('id = :id', { id: renote.id }) + .execute(); } async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) { @@ -525,7 +530,7 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri await Notes.insert(insert); } - return await Notes.findOneOrFail(insert.id); + return insert; } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { -- cgit v1.2.3-freya From e881e1bfb3d1bf0e1a453235b5fe104f276c1392 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 22 Mar 2021 10:45:07 +0900 Subject: perf(server): Reduce database query --- src/services/note/create.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'src/services/note') diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 96177e9758..4a737e8516 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -259,21 +259,21 @@ export default async (user: User, data: Option, silent = false) => new Promise { - const followings = await Followings.createQueryBuilder('following') - .andWhere(`following.followeeId = :userId`, { userId: note.userId }) - .getMany(); - - const followers = followings.map(f => f.followerId); - - for (const antenna of antennas) { - checkHitAntenna(antenna, note, user, followers).then(hit => { - if (hit) { - addNoteToAntenna(antenna, note, user); + Followings.createQueryBuilder('following') + .andWhere(`following.followeeId = :userId`, { userId: note.userId }) + .getMany() + .then(followings => { + const followers = followings.map(f => f.followerId); + Antennas.find().then(async antennas => { + for (const antenna of antennas) { + checkHitAntenna(antenna, note, user, followers).then(hit => { + if (hit) { + addNoteToAntenna(antenna, note, user); + } + }); } }); - } - }); + }); // Channel if (note.channelId) { -- cgit v1.2.3-freya