summaryrefslogtreecommitdiff
path: root/packages/backend/src/core
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-06-08 19:52:59 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-06-09 11:02:51 -0400
commitfa68751a19877474bf78a80ef7204102296f0f17 (patch)
tree63d81dbc815f0d7c07a7f7effb51db026e1d8121 /packages/backend/src/core
parentimplement userFollowersCache (diff)
downloadsharkey-fa68751a19877474bf78a80ef7204102296f0f17.tar.gz
sharkey-fa68751a19877474bf78a80ef7204102296f0f17.tar.bz2
sharkey-fa68751a19877474bf78a80ef7204102296f0f17.zip
normalize userFollowingsCache / userFollowersCache and add hibernatedUserCache to reduce the number of cache-clears and allow use of caching in many more places
Diffstat (limited to 'packages/backend/src/core')
-rw-r--r--packages/backend/src/core/AccountMoveService.ts24
-rw-r--r--packages/backend/src/core/CacheService.ts316
-rw-r--r--packages/backend/src/core/GlobalEventService.ts2
-rw-r--r--packages/backend/src/core/NoteCreateService.ts43
-rw-r--r--packages/backend/src/core/NoteEditService.ts33
-rw-r--r--packages/backend/src/core/UserFollowingService.ts119
-rw-r--r--packages/backend/src/core/UserService.ts21
-rw-r--r--packages/backend/src/core/UserSuspendService.ts10
-rw-r--r--packages/backend/src/core/activitypub/ApDeliverManagerService.ts54
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts16
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts15
-rw-r--r--packages/backend/src/core/chart/charts/federation.ts1
-rw-r--r--packages/backend/src/core/chart/charts/per-user-following.ts19
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts10
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts44
15 files changed, 343 insertions, 384 deletions
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 738026f753..e107f02796 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -26,6 +26,7 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { RoleService } from '@/core/RoleService.js';
import { AntennaService } from '@/core/AntennaService.js';
+import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class AccountMoveService {
@@ -68,6 +69,7 @@ export class AccountMoveService {
private systemAccountService: SystemAccountService,
private roleService: RoleService,
private antennaService: AntennaService,
+ private readonly cacheService: CacheService,
) {
}
@@ -107,12 +109,10 @@ export class AccountMoveService {
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
// Unfollow after 24 hours
- const followings = await this.followingsRepository.findBy({
- followerId: src.id,
- });
- this.queueService.createDelayedUnfollowJob(followings.map(following => ({
+ const followings = await this.cacheService.userFollowingsCache.fetch(src.id);
+ this.queueService.createDelayedUnfollowJob(Array.from(followings.keys()).map(followeeId => ({
from: { id: src.id },
- to: { id: following.followeeId },
+ to: { id: followeeId },
})), process.env.NODE_ENV === 'test' ? 10000 : 1000 * 60 * 60 * 24);
await this.postMoveProcess(src, dst);
@@ -138,11 +138,9 @@ export class AccountMoveService {
// follow the new account
const proxy = await this.systemAccountService.fetch('proxy');
- const followings = await this.followingsRepository.findBy({
- followeeId: src.id,
- followerHost: IsNull(), // follower is local
- followerId: Not(proxy.id),
- });
+ const followings = await this.cacheService.userFollowersCache.fetch(src.id)
+ .then(fs => Array.from(fs.values())
+ .filter(f => f.followerHost == null && f.followerId !== proxy.id));
const followJobs = followings.map(following => ({
from: { id: following.followerId },
to: { id: dst.id },
@@ -318,9 +316,9 @@ export class AccountMoveService {
await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1);
// Decrease follower counts of local followees by 1.
- const oldFollowings = await this.followingsRepository.findBy({ followerId: oldAccount.id });
- if (oldFollowings.length > 0) {
- await this.usersRepository.decrement({ id: In(oldFollowings.map(following => following.followeeId)) }, 'followersCount', 1);
+ const oldFollowings = await this.cacheService.userFollowingsCache.fetch(oldAccount.id);
+ if (oldFollowings.size > 0) {
+ await this.usersRepository.decrement({ id: In(Array.from(oldFollowings.keys())) }, 'followersCount', 1);
}
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts
index e8b26f8b9b..9c68597441 100644
--- a/packages/backend/src/core/CacheService.ts
+++ b/packages/backend/src/core/CacheService.ts
@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { In, IsNull } from 'typeorm';
-import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiNote } from '@/models/_.js';
+import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiNote, MiFollowing } from '@/models/_.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { QuantumKVCache } from '@/misc/QuantumKVCache.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -46,8 +46,9 @@ export class CacheService implements OnApplicationShutdown {
public userBlockingCache: QuantumKVCache<Set<string>>;
public userBlockedCache: QuantumKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: QuantumKVCache<Set<string>>;
- public userFollowingsCache: QuantumKVCache<Map<string, { withReplies: boolean }>>;
- public userFollowersCache: QuantumKVCache<Set<string>>;
+ public userFollowingsCache: QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>;
+ public userFollowersCache: QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>;
+ public hibernatedUserCache: QuantumKVCache<boolean>;
protected userFollowStatsCache = new MemoryKVCache<FollowStats>(1000 * 60 * 10); // 10 minutes
protected translationsCache: RedisKVCache<CachedTranslationEntity>;
@@ -89,36 +90,145 @@ export class CacheService implements OnApplicationShutdown {
this.userProfileCache = new QuantumKVCache(this.internalEventService, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }),
+ bulkFetcher: userIds => this.userProfilesRepository.findBy({ userId: In(userIds) }).then(ps => ps.map(p => [p.userId, p])),
});
this.userMutingsCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'userMutings', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
+ bulkFetcher: muterIds => this.mutingsRepository
+ .createQueryBuilder('muting')
+ .select('"muting"."muterId"', 'muterId')
+ .addSelect('array_agg("muting"."muteeId")', 'muteeIds')
+ .where({ muterId: In(muterIds) })
+ .groupBy('muting.muterId')
+ .getRawMany<{ muterId: string, muteeIds: string[] }>()
+ .then(ms => ms.map(m => [m.muterId, new Set(m.muteeIds)])),
});
this.userBlockingCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'userBlocking', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
+ bulkFetcher: blockerIds => this.blockingsRepository
+ .createQueryBuilder('blocking')
+ .select('"blocking"."blockerId"', 'blockerId')
+ .addSelect('array_agg("blocking"."blockeeId")', 'blockeeIds')
+ .where({ blockerId: In(blockerIds) })
+ .groupBy('blocking.blockerId')
+ .getRawMany<{ blockerId: string, blockeeIds: string[] }>()
+ .then(ms => ms.map(m => [m.blockerId, new Set(m.blockeeIds)])),
});
this.userBlockedCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'userBlocked', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
+ bulkFetcher: blockeeIds => this.blockingsRepository
+ .createQueryBuilder('blocking')
+ .select('"blocking"."blockeeId"', 'blockeeId')
+ .addSelect('array_agg("blocking"."blockeeId")', 'blockeeIds')
+ .where({ blockeeId: In(blockeeIds) })
+ .groupBy('blocking.blockeeId')
+ .getRawMany<{ blockeeId: string, blockerIds: string[] }>()
+ .then(ms => ms.map(m => [m.blockeeId, new Set(m.blockerIds)])),
});
this.renoteMutingsCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'renoteMutings', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
+ bulkFetcher: muterIds => this.renoteMutingsRepository
+ .createQueryBuilder('muting')
+ .select('"muting"."muterId"', 'muterId')
+ .addSelect('array_agg("muting"."muteeId")', 'muteeIds')
+ .where({ muterId: In(muterIds) })
+ .groupBy('muting.muterId')
+ .getRawMany<{ muterId: string, muteeIds: string[] }>()
+ .then(ms => ms.map(m => [m.muterId, new Set(m.muteeIds)])),
});
- this.userFollowingsCache = new QuantumKVCache<Map<string, { withReplies: boolean }>>(this.internalEventService, 'userFollowings', {
+ this.userFollowingsCache = new QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>(this.internalEventService, 'userFollowings', {
lifetime: 1000 * 60 * 30, // 30m
- fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => new Map(xs.map(f => [f.followeeId, { withReplies: f.withReplies }]))),
+ fetcher: (key) => this.followingsRepository.findBy({ followerId: key }).then(xs => new Map(xs.map(f => [f.followeeId, f]))),
+ bulkFetcher: followerIds => this.followingsRepository
+ .findBy({ followerId: In(followerIds) })
+ .then(fs => fs
+ .reduce((groups, f) => {
+ let group = groups.get(f.followerId);
+ if (!group) {
+ group = new Map();
+ groups.set(f.followerId, group);
+ }
+ group.set(f.followeeId, f);
+ return groups;
+ }, {} as Map<string, Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>)),
});
- this.userFollowersCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'userFollowers', {
+ this.userFollowersCache = new QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>(this.internalEventService, 'userFollowers', {
lifetime: 1000 * 60 * 30, // 30m
- fetcher: (key) => this.followingsRepository.find({ where: { followeeId: key }, select: ['followerId'] }).then(xs => new Set(xs.map(x => x.followerId))),
+ fetcher: followeeId => this.followingsRepository.findBy({ followeeId: followeeId }).then(xs => new Map(xs.map(x => [x.followerId, x]))),
+ bulkFetcher: followeeIds => this.followingsRepository
+ .findBy({ followeeId: In(followeeIds) })
+ .then(fs => fs
+ .reduce((groups, f) => {
+ let group = groups.get(f.followeeId);
+ if (!group) {
+ group = new Map();
+ groups.set(f.followeeId, group);
+ }
+ group.set(f.followerId, f);
+ return groups;
+ }, {} as Map<string, Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>)),
+ });
+
+ this.hibernatedUserCache = new QuantumKVCache<boolean>(this.internalEventService, 'hibernatedUsers', {
+ lifetime: 1000 * 60 * 30, // 30m
+ fetcher: async userId => {
+ const { isHibernated } = await this.usersRepository.findOneOrFail({
+ where: { id: userId },
+ select: { isHibernated: true },
+ });
+ return isHibernated;
+ },
+ bulkFetcher: async userIds => {
+ const results = await this.usersRepository.find({
+ where: { id: In(userIds) },
+ select: { id: true, isHibernated: true },
+ });
+ return results.map(({ id, isHibernated }) => [id, isHibernated]);
+ },
+ onChanged: async userIds => {
+ // We only update local copies since each process will get this event, but we can have user objects in multiple different caches.
+ // Before doing anything else we must "find" all the objects to update.
+ const userObjects = new Map<string, MiUser[]>();
+ const toUpdate: string[] = [];
+ for (const uid of userIds) {
+ const toAdd: MiUser[] = [];
+
+ const localUserById = this.localUserByIdCache.get(uid);
+ if (localUserById) toAdd.push(localUserById);
+
+ const userById = this.userByIdCache.get(uid);
+ if (userById) toAdd.push(userById);
+
+ if (toAdd.length > 0) {
+ toUpdate.push(uid);
+ userObjects.set(uid, toAdd);
+ }
+ }
+
+ // In many cases, we won't have to do anything.
+ // Skipping the DB fetch ensures that this remains a single-step synchronous process.
+ if (toUpdate.length > 0) {
+ const hibernations = await this.usersRepository.find({ where: { id: In(toUpdate) }, select: { id: true, isHibernated: true } });
+ for (const { id, isHibernated } of hibernations) {
+ const users = userObjects.get(id);
+ if (users) {
+ for (const u of users) {
+ u.isHibernated = isHibernated;
+ }
+ }
+ }
+ }
+ },
});
this.translationsCache = new RedisKVCache<CachedTranslationEntity>(this.redisClient, 'translations', {
@@ -161,6 +271,7 @@ export class CacheService implements OnApplicationShutdown {
this.renoteMutingsCache.delete(body.id),
this.userFollowingsCache.delete(body.id),
this.userFollowersCache.delete(body.id),
+ this.hibernatedUserCache.delete(body.id),
]);
}
} else {
@@ -312,142 +423,6 @@ export class CacheService implements OnApplicationShutdown {
}
@bindThis
- public async getUserFollowings(userIds: Iterable<string>): Promise<Map<string, Map<string, { withReplies: boolean }>>> {
- const followings = new Map<string, Map<string, { withReplies: boolean }>>();
-
- const toFetch: string[] = [];
- for (const userId of userIds) {
- const fromCache = this.userFollowingsCache.get(userId);
- if (fromCache) {
- followings.set(userId, fromCache);
- } else {
- toFetch.push(userId);
- }
- }
-
- if (toFetch.length > 0) {
- const fetchedFollowings = await this.followingsRepository
- .createQueryBuilder('following')
- .select([
- 'following.followerId',
- 'following.followeeId',
- 'following.withReplies',
- ])
- .where({
- followerId: In(toFetch),
- })
- .getMany();
-
- const toCache = new Map<string, Map<string, { withReplies: boolean }>>();
-
- // Pivot to a map
- for (const { followerId, followeeId, withReplies } of fetchedFollowings) {
- // Queue for cache
- let cacheMap = toCache.get(followerId);
- if (!cacheMap) {
- cacheMap = new Map();
- toCache.set(followerId, cacheMap);
- }
- cacheMap.set(followeeId, { withReplies });
-
- // Queue for return
- let returnSet = followings.get(followerId);
- if (!returnSet) {
- returnSet = new Map();
- followings.set(followerId, returnSet);
- }
- returnSet.set(followeeId, { withReplies });
- }
-
- // Update cache to speed up future calls
- this.userFollowingsCache.addMany(toCache);
- }
-
- return followings;
- }
-
- @bindThis
- public async getUserBlockers(userIds: Iterable<string>): Promise<Map<string, Set<string>>> {
- const blockers = new Map<string, Set<string>>();
-
- const toFetch: string[] = [];
- for (const userId of userIds) {
- const fromCache = this.userBlockedCache.get(userId);
- if (fromCache) {
- blockers.set(userId, fromCache);
- } else {
- toFetch.push(userId);
- }
- }
-
- if (toFetch.length > 0) {
- const fetchedBlockers = await this.blockingsRepository.createQueryBuilder('blocking')
- .select([
- 'blocking.blockerId',
- 'blocking.blockeeId',
- ])
- .where({
- blockeeId: In(toFetch),
- })
- .getMany();
-
- const toCache = new Map<string, Set<string>>();
-
- // Pivot to a map
- for (const { blockerId, blockeeId } of fetchedBlockers) {
- // Queue for cache
- let cacheSet = toCache.get(blockeeId);
- if (!cacheSet) {
- cacheSet = new Set();
- toCache.set(blockeeId, cacheSet);
- }
- cacheSet.add(blockerId);
-
- // Queue for return
- let returnSet = blockers.get(blockeeId);
- if (!returnSet) {
- returnSet = new Set();
- blockers.set(blockeeId, returnSet);
- }
- returnSet.add(blockerId);
- }
-
- // Update cache to speed up future calls
- this.userBlockedCache.addMany(toCache);
- }
-
- return blockers;
- }
-
- public async getUserProfiles(userIds: Iterable<string>): Promise<Map<string, MiUserProfile>> {
- const profiles = new Map<string, MiUserProfile>;
-
- const toFetch: string[] = [];
- for (const userId of userIds) {
- const fromCache = this.userProfileCache.get(userId);
- if (fromCache) {
- profiles.set(userId, fromCache);
- } else {
- toFetch.push(userId);
- }
- }
-
- if (toFetch.length > 0) {
- const fetched = await this.userProfilesRepository.findBy({
- userId: In(toFetch),
- });
-
- for (const profile of fetched) {
- profiles.set(profile.userId, profile);
- }
-
- const toCache = new Map(fetched.map(p => [p.userId, p]));
- this.userProfileCache.addMany(toCache);
- }
-
- return profiles;
- }
-
public async getUsers(userIds: Iterable<string>): Promise<Map<string, MiUser>> {
const users = new Map<string, MiUser>;
@@ -476,6 +451,61 @@ export class CacheService implements OnApplicationShutdown {
}
@bindThis
+ public async isFollowing(follower: string | { id: string }, followee: string | { id: string }): Promise<boolean> {
+ const followerId = typeof(follower) === 'string' ? follower : follower.id;
+ const followeeId = typeof(followee) === 'string' ? followee : followee.id;
+
+ // This lets us use whichever one is in memory, falling back to DB fetch via userFollowingsCache.
+ return this.userFollowersCache.get(followeeId)?.has(followerId)
+ ?? (await this.userFollowingsCache.fetch(followerId)).has(followeeId);
+ }
+
+ /**
+ * Returns all hibernated followers.
+ */
+ @bindThis
+ public async getHibernatedFollowers(followeeId: string): Promise<MiFollowing[]> {
+ const followers = await this.getFollowersWithHibernation(followeeId);
+ return followers.filter(f => f.isFollowerHibernated);
+ }
+
+ /**
+ * Returns all non-hibernated followers.
+ */
+ @bindThis
+ public async getNonHibernatedFollowers(followeeId: string): Promise<MiFollowing[]> {
+ const followers = await this.getFollowersWithHibernation(followeeId);
+ return followers.filter(f => !f.isFollowerHibernated);
+ }
+
+ /**
+ * Returns follower relations with populated isFollowerHibernated.
+ * If you don't need this field, then please use userFollowersCache directly for reduced overhead.
+ */
+ @bindThis
+ public async getFollowersWithHibernation(followeeId: string): Promise<MiFollowing[]> {
+ const followers = await this.userFollowersCache.fetch(followeeId);
+ const hibernations = await this.hibernatedUserCache.fetchMany(followers.keys()).then(fs => fs.reduce((map, f) => {
+ map.set(f[0], f[1]);
+ return map;
+ }, new Map<string, boolean>));
+ return Array.from(followers.values()).map(following => ({
+ ...following,
+ isFollowerHibernated: hibernations.get(following.followerId) ?? false,
+ }));
+ }
+
+ /**
+ * Refreshes follower and following relations for the given user.
+ */
+ @bindThis
+ public async refreshFollowRelationsFor(userId: string): Promise<void> {
+ const followings = await this.userFollowingsCache.refresh(userId);
+ const followees = Array.from(followings.values()).map(f => f.followeeId);
+ await this.userFollowersCache.deleteMany(followees);
+ }
+
+ @bindThis
public clear(): void {
this.userByIdCache.clear();
this.localUserByNativeTokenCache.clear();
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index de35e9db19..c146811331 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -265,7 +265,7 @@ export interface InternalEventTypes {
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
- quantumCacheUpdated: { name: string, keys: string[], op: 's' | 'd' };
+ quantumCacheUpdated: { name: string, keys: string[] };
}
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 4dceb6e953..a9f4083446 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -606,11 +606,11 @@ export class NoteCreateService implements OnApplicationShutdown {
}
if (data.reply == null) {
- // TODO: キャッシュ
- this.followingsRepository.findBy({
- followeeId: user.id,
- notify: 'normal',
- }).then(async followings => {
+ this.cacheService.userFollowersCache.fetch(user.id).then(async followingsMap => {
+ const followings = Array
+ .from(followingsMap.values())
+ .filter(f => f.notify === 'normal');
+
if (note.visibility !== 'specified') {
const isPureRenote = this.isRenote(data) && !this.isQuote(data) ? true : false;
for (const following of followings) {
@@ -948,14 +948,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// TODO: キャッシュ?
// eslint-disable-next-line prefer-const
let [followings, userListMemberships] = await Promise.all([
- this.followingsRepository.find({
- where: {
- followeeId: user.id,
- followerHost: IsNull(),
- isFollowerHibernated: false,
- },
- select: ['followerId', 'withReplies'],
- }),
+ this.cacheService.getNonHibernatedFollowers(user.id),
this.userListMembershipsRepository.find({
where: {
userId: user.id,
@@ -1072,17 +1065,19 @@ export class NoteCreateService implements OnApplicationShutdown {
});
if (hibernatedUsers.length > 0) {
- this.usersRepository.update({
- id: In(hibernatedUsers.map(x => x.id)),
- }, {
- isHibernated: true,
- });
-
- this.followingsRepository.update({
- followerId: In(hibernatedUsers.map(x => x.id)),
- }, {
- isFollowerHibernated: true,
- });
+ await Promise.all([
+ this.usersRepository.update({
+ id: In(hibernatedUsers.map(x => x.id)),
+ }, {
+ isHibernated: true,
+ }),
+ this.followingsRepository.update({
+ followerId: In(hibernatedUsers.map(x => x.id)),
+ }, {
+ isFollowerHibernated: true,
+ }),
+ this.cacheService.hibernatedUserCache.setMany(hibernatedUsers.map(x => [x.id, true])),
+ ]);
}
}
diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts
index 34af1c76dd..a359381573 100644
--- a/packages/backend/src/core/NoteEditService.ts
+++ b/packages/backend/src/core/NoteEditService.ts
@@ -833,14 +833,7 @@ export class NoteEditService implements OnApplicationShutdown {
// TODO: キャッシュ?
// eslint-disable-next-line prefer-const
let [followings, userListMemberships] = await Promise.all([
- this.followingsRepository.find({
- where: {
- followeeId: user.id,
- followerHost: IsNull(),
- isFollowerHibernated: false,
- },
- select: ['followerId', 'withReplies'],
- }),
+ this.cacheService.getNonHibernatedFollowers(user.id),
this.userListMembershipsRepository.find({
where: {
userId: user.id,
@@ -957,17 +950,19 @@ export class NoteEditService implements OnApplicationShutdown {
});
if (hibernatedUsers.length > 0) {
- this.usersRepository.update({
- id: In(hibernatedUsers.map(x => x.id)),
- }, {
- isHibernated: true,
- });
-
- this.followingsRepository.update({
- followerId: In(hibernatedUsers.map(x => x.id)),
- }, {
- isFollowerHibernated: true,
- });
+ await Promise.all([
+ this.usersRepository.update({
+ id: In(hibernatedUsers.map(x => x.id)),
+ }, {
+ isHibernated: true,
+ }),
+ this.followingsRepository.update({
+ followerId: In(hibernatedUsers.map(x => x.id)),
+ }, {
+ isFollowerHibernated: true,
+ }),
+ this.cacheService.hibernatedUserCache.setMany(hibernatedUsers.map(x => [x.id, true])),
+ ]);
}
}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 6a6c9a3000..8470872eac 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -147,12 +147,7 @@ export class UserFollowingService implements OnModuleInit {
if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
}
- if (await this.followingsRepository.exists({
- where: {
- followerId: follower.id,
- followeeId: followee.id,
- },
- })) {
+ if (await this.cacheService.isFollowing(follower, followee)) {
// すでにフォロー関係が存在している場合
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
// リモート → ローカル: acceptを送り返しておしまい
@@ -180,24 +175,14 @@ export class UserFollowingService implements OnModuleInit {
let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー
- const isFollowing = await this.followingsRepository.exists({
- where: {
- followerId: follower.id,
- followeeId: followee.id,
- },
- });
+ const isFollowing = await this.cacheService.isFollowing(follower, followee);
if (isFollowing) {
autoAccept = true;
}
// フォローしているユーザーは自動承認オプション
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
- const isFollowed = await this.followingsRepository.exists({
- where: {
- followerId: followee.id,
- followeeId: follower.id,
- },
- });
+ const isFollowed = await this.cacheService.isFollowing(followee, follower); // intentionally reversed parameters
if (isFollowed) autoAccept = true;
}
@@ -206,12 +191,7 @@ export class UserFollowingService implements OnModuleInit {
if (followee.isLocked && !autoAccept) {
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
follower,
- (oldSrc, newSrc) => this.followingsRepository.exists({
- where: {
- followeeId: followee.id,
- followerId: newSrc.id,
- },
- }),
+ (oldSrc, newSrc) => this.cacheService.isFollowing(newSrc, followee),
true,
));
}
@@ -366,32 +346,29 @@ export class UserFollowingService implements OnModuleInit {
},
silent = false,
): Promise<void> {
- const following = await this.followingsRepository.findOne({
- relations: {
- follower: true,
- followee: true,
- },
- where: {
- followerId: follower.id,
- followeeId: followee.id,
- },
- });
+ const [
+ followerUser,
+ followeeUser,
+ following,
+ ] = await Promise.all([
+ this.cacheService.findUserById(follower.id),
+ this.cacheService.findUserById(followee.id),
+ this.cacheService.userFollowingsCache.fetch(follower.id).then(fs => fs.get(followee.id)),
+ ]);
- if (following === null || !following.follower || !following.followee) {
+ if (following == null) {
this.logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
return;
}
await this.followingsRepository.delete(following.id);
+ await this.internalEventService.emit('unfollow', { followerId: follower.id, followeeId: followee.id });
- // Handled by CacheService
- // this.cacheService.userFollowingsCache.refresh(follower.id);
-
- this.decrementFollowing(following.follower, following.followee);
+ this.decrementFollowing(followerUser, followeeUser);
if (!silent && this.userEntityService.isLocalUser(follower)) {
// Publish unfollow event
- this.userEntityService.pack(followee.id, follower, {
+ this.userEntityService.pack(followeeUser, follower, {
schema: 'UserDetailedNotMe',
}).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
@@ -416,8 +393,6 @@ export class UserFollowingService implements OnModuleInit {
follower: MiUser,
followee: MiUser,
): Promise<void> {
- await this.internalEventService.emit('unfollow', { followerId: follower.id, followeeId: followee.id });
-
// Neither followee nor follower has moved.
if (!follower.movedToUri && !followee.movedToUri) {
//#region Decrement following / followers counts
@@ -691,22 +666,22 @@ export class UserFollowingService implements OnModuleInit {
*/
@bindThis
private async removeFollow(followee: Both, follower: Both): Promise<void> {
- const following = await this.followingsRepository.findOne({
- relations: {
- followee: true,
- follower: true,
- },
- where: {
- followeeId: followee.id,
- followerId: follower.id,
- },
- });
+ const [
+ followerUser,
+ followeeUser,
+ following,
+ ] = await Promise.all([
+ this.cacheService.findUserById(follower.id),
+ this.cacheService.findUserById(followee.id),
+ this.cacheService.userFollowingsCache.fetch(follower.id).then(fs => fs.get(followee.id)),
+ ]);
- if (!following || !following.followee || !following.follower) return;
+ if (!following) return;
await this.followingsRepository.delete(following.id);
+ await this.internalEventService.emit('unfollow', { followerId: follower.id, followeeId: followee.id });
- this.decrementFollowing(following.follower, following.followee);
+ this.decrementFollowing(followerUser, followeeUser);
}
/**
@@ -737,36 +712,26 @@ export class UserFollowingService implements OnModuleInit {
}
@bindThis
- public getFollowees(userId: MiUser['id']) {
- return this.followingsRepository.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: userId })
- .getMany();
+ public async getFollowees(userId: MiUser['id']) {
+ const followings = await this.cacheService.userFollowingsCache.fetch(userId);
+ return Array.from(followings.values());
}
@bindThis
- public isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) {
- return this.followingsRepository.exists({
- where: {
- followerId,
- followeeId,
- },
- });
+ public async isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) {
+ return this.cacheService.isFollowing(followerId, followeeId);
}
@bindThis
public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) {
- const count = await this.followingsRepository.createQueryBuilder('following')
- .where(new Brackets(qb => {
- qb.where('following.followerId = :aUserId', { aUserId })
- .andWhere('following.followeeId = :bUserId', { bUserId });
- }))
- .orWhere(new Brackets(qb => {
- qb.where('following.followerId = :bUserId', { bUserId })
- .andWhere('following.followeeId = :aUserId', { aUserId });
- }))
- .getCount();
+ const [
+ isFollowing,
+ isFollowed,
+ ] = await Promise.all([
+ this.isFollowing(aUserId, bUserId),
+ this.isFollowing(bUserId, aUserId),
+ ]);
- return count === 2;
+ return isFollowing && isFollowed;
}
}
diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts
index 1f471513f3..4a04910105 100644
--- a/packages/backend/src/core/UserService.ts
+++ b/packages/backend/src/core/UserService.ts
@@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class UserService {
@@ -20,6 +21,7 @@ export class UserService {
private followingsRepository: FollowingsRepository,
private systemWebhookService: SystemWebhookService,
private userEntityService: UserEntityService,
+ private readonly cacheService: CacheService,
) {
}
@@ -38,14 +40,17 @@ export class UserService {
});
const wokeUp = result.isHibernated;
if (wokeUp) {
- this.usersRepository.update(user.id, {
- isHibernated: false,
- });
- this.followingsRepository.update({
- followerId: user.id,
- }, {
- isFollowerHibernated: false,
- });
+ await Promise.all([
+ this.usersRepository.update(user.id, {
+ isHibernated: false,
+ }),
+ this.followingsRepository.update({
+ followerId: user.id,
+ }, {
+ isFollowerHibernated: false,
+ }),
+ this.cacheService.hibernatedUserCache.set(user.id, false),
+ ]);
}
} else {
this.usersRepository.update(user.id, {
diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts
index 30dcaa6f7d..f375dff862 100644
--- a/packages/backend/src/core/UserSuspendService.ts
+++ b/packages/backend/src/core/UserSuspendService.ts
@@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js';
import { RelationshipJobData } from '@/queue/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
+import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class UserSuspendService {
@@ -34,6 +35,7 @@ export class UserSuspendService {
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
+ private readonly cacheService: CacheService,
) {
}
@@ -143,12 +145,8 @@ export class UserSuspendService {
@bindThis
private async unFollowAll(follower: MiUser) {
- const followings = await this.followingsRepository.find({
- where: {
- followerId: follower.id,
- followeeId: Not(IsNull()),
- },
- });
+ const followings = await this.cacheService.userFollowingsCache.fetch(follower.id)
+ .then(fs => Array.from(fs.values()).filter(f => f.followeeHost != null));
const jobs: RelationshipJobData[] = [];
for (const following of followings) {
diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts
index 746af41f55..86747f2508 100644
--- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts
+++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts
@@ -5,7 +5,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
-import { UnrecoverableError } from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository } from '@/models/_.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
@@ -14,6 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import type { IActivity } from '@/core/activitypub/type.js';
import { ThinUser } from '@/queue/types.js';
+import { CacheService } from '@/core/CacheService.js';
interface IRecipe {
type: string;
@@ -41,16 +41,14 @@ class DeliverManager {
/**
* Constructor
- * @param userEntityService
- * @param followingsRepository
* @param queueService
+ * @param cacheService
* @param actor Actor
* @param activity Activity to deliver
*/
constructor(
- private userEntityService: UserEntityService,
- private followingsRepository: FollowingsRepository,
private queueService: QueueService,
+ private readonly cacheService: CacheService,
actor: { id: MiUser['id']; host: null; },
activity: IActivity | null,
@@ -114,24 +112,23 @@ class DeliverManager {
// Process follower recipes first to avoid duplication when processing direct recipes later.
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
- // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
- const followers = await this.followingsRepository.find({
- where: {
- followeeId: this.actor.id,
- followerHost: Not(IsNull()),
- },
- select: {
- followerSharedInbox: true,
- followerInbox: true,
- followerId: true,
- },
- });
+ const followers = await this.cacheService.userFollowingsCache
+ .fetch(this.actor.id)
+ .then(f => Array
+ .from(f.values())
+ .filter(f => f.followerHost != null)
+ .map(f => ({
+ followerInbox: f.followerInbox,
+ followerSharedInbox: f.followerSharedInbox,
+ })));
for (const following of followers) {
- const inbox = following.followerSharedInbox ?? following.followerInbox;
- if (inbox === null) throw new UnrecoverableError(`deliver failed for ${this.actor.id}: follower ${following.followerId} inbox is null`);
- inboxes.set(inbox, following.followerSharedInbox != null);
+ if (following.followerSharedInbox) {
+ inboxes.set(following.followerSharedInbox, true);
+ } else if (following.followerInbox) {
+ inboxes.set(following.followerInbox, false);
+ }
}
}
@@ -153,11 +150,8 @@ class DeliverManager {
@Injectable()
export class ApDeliverManagerService {
constructor(
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
-
- private userEntityService: UserEntityService,
private queueService: QueueService,
+ private readonly cacheService: CacheService,
) {
}
@@ -169,9 +163,8 @@ export class ApDeliverManagerService {
@bindThis
public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
const manager = new DeliverManager(
- this.userEntityService,
- this.followingsRepository,
this.queueService,
+ this.cacheService,
actor,
activity,
);
@@ -188,9 +181,8 @@ export class ApDeliverManagerService {
@bindThis
public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise<void> {
const manager = new DeliverManager(
- this.userEntityService,
- this.followingsRepository,
this.queueService,
+ this.cacheService,
actor,
activity,
);
@@ -207,9 +199,8 @@ export class ApDeliverManagerService {
@bindThis
public async deliverToUsers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, targets: MiRemoteUser[]): Promise<void> {
const manager = new DeliverManager(
- this.userEntityService,
- this.followingsRepository,
this.queueService,
+ this.cacheService,
actor,
activity,
);
@@ -220,9 +211,8 @@ export class ApDeliverManagerService {
@bindThis
public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager(
- this.userEntityService,
- this.followingsRepository,
this.queueService,
+ this.cacheService,
actor,
activity,
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 7c26deb00f..009d4cbd39 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -37,6 +37,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import FederationChart from '@/core/chart/charts/federation.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
+import { CacheService } from '@/core/CacheService.js';
import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost, isActivity, IObjectWithId } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -98,6 +99,7 @@ export class ApInboxService {
private readonly instanceChart: InstanceChart,
private readonly federationChart: FederationChart,
private readonly updateInstanceQueue: UpdateInstanceQueue,
+ private readonly cacheService: CacheService,
) {
this.logger = this.apLoggerService.logger;
}
@@ -766,12 +768,7 @@ export class ApInboxService {
return 'skip: follower not found';
}
- const isFollowing = await this.followingsRepository.exists({
- where: {
- followerId: follower.id,
- followeeId: actor.id,
- },
- });
+ const isFollowing = await this.cacheService.userFollowingsCache.fetch(follower.id).then(f => f.has(actor.id));
if (isFollowing) {
await this.userFollowingService.unfollow(follower, actor);
@@ -830,12 +827,7 @@ export class ApInboxService {
},
});
- const isFollowing = await this.followingsRepository.exists({
- where: {
- followerId: actor.id,
- followeeId: followee.id,
- },
- });
+ const isFollowing = await this.cacheService.userFollowingsCache.fetch(actor.id).then(f => f.has(followee.id));
if (requestExist) {
await this.userFollowingService.cancelFollowRequest(followee, actor);
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index b7aa036068..29f7459219 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -741,10 +741,17 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
this.hashtagService.updateUsertags(exist, tags);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
- await this.followingsRepository.update(
- { followerId: exist.id },
- { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null },
- );
+ if (exist.inbox !== person.inbox || exist.sharedInbox !== (person.sharedInbox ?? person.endpoints?.sharedInbox)) {
+ await this.followingsRepository.update(
+ { followerId: exist.id },
+ {
+ followerInbox: person.inbox,
+ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
+ },
+ );
+
+ await this.cacheService.refreshFollowRelationsFor(exist.id);
+ }
await this.updateFeatured(exist.id, resolver).catch(err => {
// Permanent error implies hidden or inaccessible, which is a normal thing.
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index b6db6f5454..4bbb5437cc 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -44,6 +44,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
}
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
+ // TODO optimization: replace these with exists()
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
.select('f.followerHost')
.where('f.followerHost IS NOT NULL');
diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts
index 588ac638de..8d75a30e9a 100644
--- a/packages/backend/src/core/chart/charts/per-user-following.ts
+++ b/packages/backend/src/core/chart/charts/per-user-following.ts
@@ -15,6 +15,7 @@ import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-following.js';
import type { KVs } from '../core.js';
+import { CacheService } from '@/core/CacheService.js';
/**
* ユーザーごとのフォローに関するチャート
@@ -31,23 +32,25 @@ export default class PerUserFollowingChart extends Chart<typeof schema> { // esl
private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
+ private readonly cacheService: CacheService,
) {
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
const [
- localFollowingsCount,
- localFollowersCount,
- remoteFollowingsCount,
- remoteFollowersCount,
+ followees,
+ followers,
] = await Promise.all([
- this.followingsRepository.countBy({ followerId: group, followeeHost: IsNull() }),
- this.followingsRepository.countBy({ followeeId: group, followerHost: IsNull() }),
- this.followingsRepository.countBy({ followerId: group, followeeHost: Not(IsNull()) }),
- this.followingsRepository.countBy({ followeeId: group, followerHost: Not(IsNull()) }),
+ this.cacheService.userFollowingsCache.fetch(group).then(fs => Array.from(fs.values())),
+ this.cacheService.userFollowersCache.fetch(group).then(fs => Array.from(fs.values())),
]);
+ const localFollowingsCount = followees.reduce((sum, f) => sum + (f.followeeHost == null ? 1 : 0), 0);
+ const localFollowersCount = followers.reduce((sum, f) => sum + (f.followerHost == null ? 1 : 0), 0);
+ const remoteFollowingsCount = followees.reduce((sum, f) => sum + (f.followeeHost == null ? 0 : 1), 0);
+ const remoteFollowersCount = followers.reduce((sum, f) => sum + (f.followerHost == null ? 0 : 1), 0);
+
return {
'local.followings.total': localFollowingsCount,
'local.followers.total': localFollowersCount,
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 3af66b220d..9b447a4064 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -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, MiPollVote, MiPoll, MiChannel } from '@/models/_.js';
+import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, MiPollVote, MiPoll, MiChannel, MiFollowing } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
@@ -133,7 +133,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis
public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null, hint?: {
- myFollowing?: ReadonlyMap<string, { withReplies: boolean }>,
+ myFollowing?: ReadonlyMap<string, unknown>,
myBlockers?: ReadonlySet<string>,
}): Promise<void> {
if (meId === packedNote.userId) return;
@@ -416,7 +416,7 @@ 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, Map<string, { withReplies: boolean }>>;
+ userFollowings: Map<string, Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>;
userBlockers: Map<string, Set<string>>;
polls: Map<string, MiPoll>;
pollVotes: Map<string, Map<string, MiPollVote[]>>;
@@ -659,9 +659,9 @@ export class NoteEntityService implements OnModuleInit {
// mentionHandles
this.getUserHandles(Array.from(mentionedUsers)),
// userFollowings
- this.cacheService.getUserFollowings(userIds),
+ this.cacheService.userFollowingsCache.fetchMany(userIds).then(fs => new Map(fs)),
// userBlockers
- this.cacheService.getUserBlockers(userIds),
+ this.cacheService.userBlockedCache.fetchMany(userIds).then(bs => new Map(bs)),
// polls
this.pollsRepository.findBy({ noteId: In(noteIds) })
.then(polls => new Map(polls.map(p => [p.noteId, p]))),
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 8ed482af6f..aecbaa7fd5 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -79,7 +79,7 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
export type UserRelation = {
id: MiUser['id']
- following: MiFollowing | null,
+ following: Omit<MiFollowing, 'isFollowerHibernated'> | null,
isFollowing: boolean
isFollowed: boolean
hasPendingFollowRequestFromYou: boolean
@@ -197,16 +197,8 @@ export class UserEntityService implements OnModuleInit {
memo,
mutedInstances,
] = await Promise.all([
- this.followingsRepository.findOneBy({
- followerId: me,
- followeeId: target,
- }),
- this.followingsRepository.exists({
- where: {
- followerId: target,
- followeeId: me,
- },
- }),
+ this.cacheService.userFollowingsCache.fetch(me).then(f => f.get(target) ?? null),
+ this.cacheService.userFollowingsCache.fetch(target).then(f => f.has(me)),
this.followRequestsRepository.exists({
where: {
followerId: me,
@@ -227,8 +219,7 @@ export class UserEntityService implements OnModuleInit {
.then(mutings => mutings.has(target)),
this.cacheService.renoteMutingsCache.fetch(me)
.then(mutings => mutings.has(target)),
- this.cacheService.userByIdCache.fetch(target, () => this.usersRepository.findOneByOrFail({ id: target }))
- .then(user => user.host),
+ this.cacheService.findUserById(target).then(u => u.host),
this.userMemosRepository.createQueryBuilder('m')
.select('m.memo')
.where({ userId: me, targetUserId: target })
@@ -271,13 +262,8 @@ export class UserEntityService implements OnModuleInit {
memos,
mutedInstances,
] = await Promise.all([
- this.followingsRepository.findBy({ followerId: me })
- .then(f => new Map(f.map(it => [it.followeeId, it]))),
- this.followingsRepository.createQueryBuilder('f')
- .select('f.followerId')
- .where('f.followeeId = :me', { me })
- .getRawMany<{ f_followerId: string }>()
- .then(it => it.map(it => it.f_followerId)),
+ this.cacheService.userFollowingsCache.fetch(me),
+ this.cacheService.userFollowersCache.fetch(me),
this.followRequestsRepository.createQueryBuilder('f')
.select('f.followeeId')
.where('f.followerId = :me', { me })
@@ -322,7 +308,7 @@ export class UserEntityService implements OnModuleInit {
id: target,
following: following,
isFollowing: following != null,
- isFollowed: followees.includes(target),
+ isFollowed: followees.has(target),
hasPendingFollowRequestFromYou: followersRequests.includes(target),
hasPendingFollowRequestToYou: followeesRequests.includes(target),
isBlocking: blockees.has(target),
@@ -354,7 +340,7 @@ export class UserEntityService implements OnModuleInit {
return false; // TODO
}
- // TODO make redis calls in MULTI?
+ // TODO optimization: make redis calls in MULTI
@bindThis
public async getNotificationsInfo(userId: MiUser['id']): Promise<{
hasUnread: boolean;
@@ -789,11 +775,11 @@ export class UserEntityService implements OnModuleInit {
.map(user => user.host)
.filter((host): host is string => host != null));
- const _profilesFromUsers: MiUserProfile[] = [];
+ const _profilesFromUsers: [string, MiUserProfile][] = [];
const _profilesToFetch: string[] = [];
for (const user of _users) {
if (user.userProfile) {
- _profilesFromUsers.push(user.userProfile);
+ _profilesFromUsers.push([user.id, user.userProfile]);
} else {
_profilesToFetch.push(user.id);
}
@@ -803,13 +789,7 @@ export class UserEntityService implements OnModuleInit {
const [profilesMap, userMemos, userRelations, pinNotes, userIdsByUri, instances, securityKeyCounts, pendingReceivedFollows, pendingSentFollows] = await Promise.all([
// profilesMap
- this.cacheService.getUserProfiles(_profilesToFetch)
- .then(profiles => {
- for (const profile of _profilesFromUsers) {
- profiles.set(profile.userId, profile);
- }
- return profiles;
- }),
+ this.cacheService.userProfileCache.fetchMany(_profilesToFetch).then(profiles => new Map(profiles.concat(_profilesFromUsers))),
// userMemos
isDetailed && meId ? this.userMemosRepository.findBy({ userId: meId })
.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo]))) : new Map(),
@@ -857,7 +837,7 @@ export class UserEntityService implements OnModuleInit {
.groupBy('key.userId')
.getRawMany<{ userId: string, userCount: number }>()
.then(counts => new Map(counts.map(c => [c.userId, c.userCount]))) : new Map(),
- // TODO check query performance
+ // TODO optimization: cache follow requests
// pendingReceivedFollows
isDetailedAndMe ? this.followRequestsRepository.createQueryBuilder('req')
.select('req.followeeId', 'followeeId')