use more bulk hints in NoteEntityService / UserEntityService, and run the packMany queries in parallel
This commit is contained in:
@@ -11,7 +11,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, MiPollVote, MiPoll, MiChannel } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DebounceLoader } from '@/misc/loader.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
@@ -26,13 +26,13 @@ import type { UserEntityService } from './UserEntityService.js';
|
||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
|
||||
// is-renote.tsとよしなにリンク
|
||||
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
|
||||
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id'] } {
|
||||
return (
|
||||
note.renote != null &&
|
||||
note.reply == null &&
|
||||
note.renoteId != null &&
|
||||
note.replyId == null &&
|
||||
note.text == null &&
|
||||
note.cw == null &&
|
||||
(note.fileIds == null || note.fileIds.length === 0) &&
|
||||
note.fileIds.length === 0 &&
|
||||
!note.hasPoll
|
||||
);
|
||||
}
|
||||
@@ -132,7 +132,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
|
||||
public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null, hint?: {
|
||||
myFollowing?: ReadonlySet<string>,
|
||||
myBlockers?: ReadonlySet<string>,
|
||||
}): Promise<void> {
|
||||
if (meId === packedNote.userId) return;
|
||||
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
@@ -188,14 +191,9 @@ export class NoteEntityService implements OnModuleInit {
|
||||
} else if (packedNote.renote && (meId === packedNote.renote.userId)) {
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
});
|
||||
const isFollowing = hint?.myFollowing
|
||||
? hint.myFollowing.has(packedNote.userId)
|
||||
: (await this.cacheService.userFollowingsCache.fetch(meId))[packedNote.userId] != null;
|
||||
|
||||
hide = !isFollowing;
|
||||
}
|
||||
@@ -211,7 +209,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
if (!hide && meId && packedNote.userId !== meId) {
|
||||
const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId);
|
||||
const blockers = hint?.myBlockers ?? await this.cacheService.userBlockedCache.fetch(meId);
|
||||
const isBlocked = blockers.has(packedNote.userId);
|
||||
|
||||
if (isBlocked) hide = true;
|
||||
}
|
||||
@@ -235,8 +234,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async populatePoll(note: MiNote, meId: MiUser['id'] | null) {
|
||||
const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id });
|
||||
private async populatePoll(note: MiNote, meId: MiUser['id'] | null, hint?: {
|
||||
poll?: MiPoll,
|
||||
myVotes?: MiPollVote[],
|
||||
}) {
|
||||
const poll = hint?.poll ?? await this.pollsRepository.findOneByOrFail({ noteId: note.id });
|
||||
const choices = poll.choices.map(c => ({
|
||||
text: c,
|
||||
votes: poll.votes[poll.choices.indexOf(c)],
|
||||
@@ -245,7 +247,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
|
||||
if (meId) {
|
||||
if (poll.multiple) {
|
||||
const votes = await this.pollVotesRepository.findBy({
|
||||
const votes = hint?.myVotes ?? await this.pollVotesRepository.findBy({
|
||||
userId: meId,
|
||||
noteId: note.id,
|
||||
});
|
||||
@@ -255,7 +257,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
choices[myChoice].isVoted = true;
|
||||
}
|
||||
} else {
|
||||
const vote = await this.pollVotesRepository.findOneBy({
|
||||
const vote = hint?.myVotes ? hint.myVotes[0] : await this.pollVotesRepository.findOneBy({
|
||||
userId: meId,
|
||||
noteId: note.id,
|
||||
});
|
||||
@@ -317,7 +319,12 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
|
||||
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null, hint?: {
|
||||
myFollowing?: ReadonlySet<string>,
|
||||
myBlocking?: ReadonlySet<string>,
|
||||
myBlockers?: ReadonlySet<string>,
|
||||
me?: Pick<MiUser, 'host'> | null,
|
||||
}): Promise<boolean> {
|
||||
// This code must always be synchronized with the checks in generateVisibilityQuery.
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (note.visibility === 'specified') {
|
||||
@@ -345,16 +352,20 @@ export class NoteEntityService implements OnModuleInit {
|
||||
return true;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const [blocked, following, user] = await Promise.all([
|
||||
this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)),
|
||||
this.followingsRepository.count({
|
||||
where: {
|
||||
const [blocked, following, userHost] = await Promise.all([
|
||||
hint?.myBlocking
|
||||
? hint.myBlocking.has(note.userId)
|
||||
: this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)),
|
||||
hint?.myFollowing
|
||||
? hint.myFollowing.has(note.userId)
|
||||
: this.followingsRepository.existsBy({
|
||||
followeeId: note.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
take: 1,
|
||||
}),
|
||||
this.usersRepository.findOneByOrFail({ id: meId }),
|
||||
}),
|
||||
hint?.me !== undefined
|
||||
? (hint.me?.host ?? null)
|
||||
: this.cacheService.userByIdCache.fetch(meId, () => this.usersRepository.findOneByOrFail({ id: meId }))
|
||||
.then(me => me.host),
|
||||
]);
|
||||
|
||||
if (blocked) return false;
|
||||
@@ -366,12 +377,13 @@ export class NoteEntityService implements OnModuleInit {
|
||||
in which case we can never know the following. Instead we have
|
||||
to assume that the users are following each other.
|
||||
*/
|
||||
return following > 0 || (note.userHost != null && user.host != null);
|
||||
return following || (note.userHost != null && userHost != null);
|
||||
}
|
||||
}
|
||||
|
||||
if (meId != null) {
|
||||
const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(note.userId);
|
||||
const blockers = hint?.myBlockers ?? await this.cacheService.userBlockedCache.fetch(meId);
|
||||
const isBlocked = blockers.has(note.userId);
|
||||
|
||||
if (isBlocked) return false;
|
||||
}
|
||||
@@ -408,6 +420,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
||||
mentionHandles: Record<string, string | undefined>;
|
||||
userFollowings: Map<string, Set<string>>;
|
||||
userBlockers: Map<string, Set<string>>;
|
||||
polls: Map<string, MiPoll>;
|
||||
pollVotes: Map<string, Map<string, MiPollVote[]>>;
|
||||
channels: Map<string, MiChannel>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'Note'>> {
|
||||
@@ -437,9 +454,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
const channel = note.channelId
|
||||
? note.channel
|
||||
? note.channel
|
||||
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
||||
? (opts._hint_?.channels.get(note.channelId) ?? note.channel ?? await this.channelsRepository.findOneBy({ id: note.channelId }))
|
||||
: null;
|
||||
|
||||
const reactionEmojiNames = Object.keys(reactions)
|
||||
@@ -485,7 +500,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||
mentionHandles: note.mentions.length > 0 ? this.getUserHandles(note.mentions, options?._hint_?.mentionHandles) : undefined,
|
||||
uri: note.uri ?? undefined,
|
||||
url: note.url ?? undefined,
|
||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||
poll: note.hasPoll ? this.populatePoll(note, meId, {
|
||||
poll: opts._hint_?.polls.get(note.id),
|
||||
myVotes: opts._hint_?.pollVotes.get(note.id)?.get(note.userId),
|
||||
}) : undefined,
|
||||
|
||||
...(meId && Object.keys(reactions).length > 0 ? {
|
||||
myReaction: this.populateMyReaction({
|
||||
@@ -518,7 +536,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||
this.treatVisibility(packed);
|
||||
|
||||
if (!opts.skipHide) {
|
||||
await this.hideNote(packed, meId);
|
||||
await this.hideNote(packed, meId, meId == null ? undefined : {
|
||||
myFollowing: opts._hint_?.userFollowings.get(meId),
|
||||
myBlockers: opts._hint_?.userBlockers.get(meId),
|
||||
});
|
||||
}
|
||||
|
||||
return packed;
|
||||
@@ -536,69 +557,53 @@ export class NoteEntityService implements OnModuleInit {
|
||||
if (notes.length === 0) return [];
|
||||
|
||||
const targetNotes: MiNote[] = [];
|
||||
const targetNotesToFetch: string[] = [];
|
||||
for (const note of notes) {
|
||||
if (isPureRenote(note)) {
|
||||
// we may need to fetch 'my reaction' for renote target.
|
||||
targetNotes.push(note.renote);
|
||||
if (note.renote.reply) {
|
||||
// idem if the renote is also a reply.
|
||||
targetNotes.push(note.renote.reply);
|
||||
if (note.renote) {
|
||||
targetNotes.push(note.renote);
|
||||
if (note.renote.reply) {
|
||||
// idem if the renote is also a reply.
|
||||
targetNotes.push(note.renote.reply);
|
||||
}
|
||||
} else if (options?.detail) {
|
||||
targetNotesToFetch.push(note.renoteId);
|
||||
}
|
||||
} else {
|
||||
if (note.reply) {
|
||||
// idem for OP of a regular reply.
|
||||
targetNotes.push(note.reply);
|
||||
} else if (note.replyId && options?.detail) {
|
||||
targetNotesToFetch.push(note.replyId);
|
||||
}
|
||||
|
||||
targetNotes.push(note);
|
||||
}
|
||||
}
|
||||
|
||||
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||
if (meId) {
|
||||
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
|
||||
|
||||
for (const note of targetNotes) {
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.id, null);
|
||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
||||
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
||||
if (pairInBuffer) {
|
||||
myReactionsMap.set(note.id, pairInBuffer[1]);
|
||||
} else {
|
||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
|
||||
userId: meId,
|
||||
noteId: In(Array.from(idsNeedFetchMyReaction)),
|
||||
}) : [];
|
||||
|
||||
for (const id of idsNeedFetchMyReaction) {
|
||||
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
|
||||
}
|
||||
// Populate any relations that weren't included in the source
|
||||
if (targetNotesToFetch.length > 0) {
|
||||
const newNotes = await this.notesRepository.find({
|
||||
where: {
|
||||
id: In(targetNotesToFetch),
|
||||
},
|
||||
relations: {
|
||||
user: true,
|
||||
reply: true,
|
||||
renote: true,
|
||||
channel: true,
|
||||
},
|
||||
});
|
||||
targetNotes.push(...newNotes);
|
||||
}
|
||||
|
||||
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
|
||||
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
|
||||
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
|
||||
const users = [
|
||||
...notes.map(({ user, userId }) => user ?? userId),
|
||||
...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
|
||||
...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
|
||||
...notes.map(({ reply, replyUserId }) => reply?.user ?? replyUserId).filter(x => x != null),
|
||||
...notes.map(({ renote, renoteUserId }) => renote?.user ?? renoteUserId).filter(x => x != null),
|
||||
];
|
||||
const packedUsers = await this.userEntityService.packMany(users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
|
||||
// Recursively add all mentioned users from all notes + replies + renotes
|
||||
const allMentionedUsers = targetNotes.reduce((users, note) => {
|
||||
@@ -607,7 +612,47 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
return users;
|
||||
}, new Set<string>());
|
||||
const mentionHandles = await this.getUserHandles(Array.from(allMentionedUsers));
|
||||
|
||||
const userIds = Array.from(new Set(users.map(u => typeof(u) === 'string' ? u : u.id)));
|
||||
const noteIds = Array.from(new Set(targetNotes.map(n => n.id)));
|
||||
const [{ bufferedReactions, myReactionsMap }, packedFiles, packedUsers, mentionHandles, userFollowings, userBlockers, polls, pollVotes, channels] = await Promise.all([
|
||||
// bufferedReactions & myReactionsMap
|
||||
this.getReactions(targetNotes, me),
|
||||
// packedFiles
|
||||
this.driveFileEntityService.packManyByIdsMap(fileIds),
|
||||
// packedUsers
|
||||
this.userEntityService.packMany(users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u]))),
|
||||
// mentionHandles
|
||||
this.getUserHandles(Array.from(allMentionedUsers)),
|
||||
// userFollowings
|
||||
this.cacheService.getUserFollowings(userIds),
|
||||
// userBlockers
|
||||
this.cacheService.getUserBlockers(userIds),
|
||||
// polls
|
||||
this.pollsRepository.findBy({ noteId: In(noteIds) })
|
||||
.then(polls => new Map(polls.map(p => [p.noteId, p]))),
|
||||
// pollVotes
|
||||
this.pollVotesRepository.findBy({ noteId: In(noteIds), userId: In(userIds) })
|
||||
.then(votes => votes.reduce((noteMap, vote) => {
|
||||
let userMap = noteMap.get(vote.noteId);
|
||||
if (!userMap) {
|
||||
userMap = new Map<string, MiPollVote[]>();
|
||||
noteMap.set(vote.noteId, userMap);
|
||||
}
|
||||
let voteList = userMap.get(vote.userId);
|
||||
if (!voteList) {
|
||||
voteList = [];
|
||||
userMap.set(vote.userId, voteList);
|
||||
}
|
||||
voteList.push(vote);
|
||||
return noteMap;
|
||||
}, new Map<string, Map<string, MiPollVote[]>>)),
|
||||
// channels
|
||||
this.getChannels(targetNotes),
|
||||
// (not returned)
|
||||
this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)),
|
||||
]);
|
||||
|
||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
@@ -617,6 +662,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||
packedFiles,
|
||||
packedUsers,
|
||||
mentionHandles,
|
||||
userFollowings,
|
||||
userBlockers,
|
||||
polls,
|
||||
pollVotes,
|
||||
channels,
|
||||
},
|
||||
})));
|
||||
}
|
||||
@@ -685,6 +735,68 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}, {} as Record<string, string | undefined>);
|
||||
}
|
||||
|
||||
private async getChannels(notes: MiNote[]): Promise<Map<string, MiChannel>> {
|
||||
const channels = new Map<string, MiChannel>();
|
||||
const channelsToFetch = new Set<string>();
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.channel) {
|
||||
channels.set(note.channel.id, note.channel);
|
||||
} else if (note.channelId) {
|
||||
channelsToFetch.add(note.channelId);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelsToFetch.size > 0) {
|
||||
const newChannels = await this.channelsRepository.findBy({
|
||||
id: In(Array.from(channelsToFetch)),
|
||||
});
|
||||
for (const channel of newChannels) {
|
||||
channels.set(channel.id, channel);
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
private async getReactions(notes: MiNote[], me: { id: string } | null | undefined) {
|
||||
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||
if (meId) {
|
||||
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
|
||||
|
||||
for (const note of notes) {
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.id, null);
|
||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
||||
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
||||
if (pairInBuffer) {
|
||||
myReactionsMap.set(note.id, pairInBuffer[1]);
|
||||
} else {
|
||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
|
||||
userId: meId,
|
||||
noteId: In(Array.from(idsNeedFetchMyReaction)),
|
||||
}) : [];
|
||||
|
||||
for (const id of idsNeedFetchMyReaction) {
|
||||
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
return { bufferedReactions, myReactionsMap };
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public genLocalNoteUri(noteId: string): string {
|
||||
return `${this.config.url}/notes/${noteId}`;
|
||||
|
||||
Reference in New Issue
Block a user