From 01bb4dc541e3b64fb8169f683a0cbcbd0bdd19f7 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 14 Sep 2025 10:44:47 -0400 Subject: [PATCH] add global event "usersUpdated" for bulk user updates --- packages/backend/src/core/CacheService.ts | 99 ++++++++++++++----- .../backend/src/core/GlobalEventService.ts | 1 + 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index b257e4d49d..82952a38c7 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -356,6 +356,7 @@ export class CacheService implements OnApplicationShutdown { // TODO bulk fetcher }); + this.internalEventService.on('usersUpdated', this.onBulkUserEvent); this.internalEventService.on('userChangeSuspendedState', this.onUserEvent); this.internalEventService.on('userChangeDeletedState', this.onUserEvent); this.internalEventService.on('remoteUserUpdated', this.onUserEvent); @@ -375,44 +376,89 @@ export class CacheService implements OnApplicationShutdown { this.internalEventService.on('userListMemberBulkRemoved', this.onListMemberEvent, { ignoreRemote: true }); } + @bindThis + private async onBulkUserEvent(body: InternalEventTypes[E], _: E, isLocal: boolean): Promise { + if (body.ids.length === 0) return; + + // Update quantum caches + if (isLocal) { + // Contains IDs of all lists where this user is a member. + const userListMemberships = this.listUserMembershipsCache + .entries() + .filter(e => body.ids.some(id => e[1].has(id))) + .map(e => e[0]) + .toArray(); + + await Promise.all([ + this.userProfileCache.deleteMany(body.ids), + this.userMutingsCache.deleteMany(body.ids), + this.userMutedCache.deleteMany(body.ids), + this.userBlockingCache.deleteMany(body.ids), + this.userBlockedCache.deleteMany(body.ids), + this.renoteMutingsCache.deleteMany(body.ids), + this.userFollowingsCache.deleteMany(body.ids), + this.userFollowersCache.deleteMany(body.ids), + this.hibernatedUserCache.deleteMany(body.ids), + this.threadMutingsCache.deleteMany(body.ids), + this.noteMutingsCache.deleteMany(body.ids), + this.userListMembershipsCache.deleteMany(body.ids), + this.listUserMembershipsCache.deleteMany(userListMemberships), + ]); + } + + // Update other caches + const users = await this.usersRepository.findBy({ id: In(body.ids) }); + for (const id of body.ids) { + const user = users.find(u => u.id === id); + this.updateMkUserCaches({ id }, user ?? null); + } + } + @bindThis private async onUserEvent(body: InternalEventTypes[E], _: E, isLocal: boolean): Promise { + // Update quantum caches + if (isLocal) { + // Contains IDs of all lists where this user is a member. + const userListMemberships = this.listUserMembershipsCache + .entries() + .filter(e => e[1].has(body.id)) + .map(e => e[0]) + .toArray(); + + await Promise.all([ + this.userProfileCache.delete(body.id), + this.userMutingsCache.delete(body.id), + this.userBlockingCache.delete(body.id), + this.userBlockedCache.delete(body.id), + this.renoteMutingsCache.delete(body.id), + this.userFollowingsCache.delete(body.id), + this.userFollowersCache.delete(body.id), + this.hibernatedUserCache.delete(body.id), + this.threadMutingsCache.delete(body.id), + this.noteMutingsCache.delete(body.id), + this.userListMembershipsCache.delete(body.id), + this.listUserMembershipsCache.deleteMany(userListMemberships), + ]); + } + + // Update other caches + const user = await this.usersRepository.findOneBy({ id: body.id }); + this.updateMkUserCaches(body, user); + } + + // This is here purely to help git line up MK's original code with our changes + @bindThis + private updateMkUserCaches(body: { id: string }, user: MiUser | null): void { { { { - const user = await this.usersRepository.findOneBy({ id: body.id }); if (user == null) { this.userByIdCache.delete(body.id); - this.localUserByIdCache.delete(body.id); for (const [k, v] of this.uriPersonCache.entries) { if (v.value?.id === body.id) { this.uriPersonCache.delete(k); } } - - // Contains IDs of all lists where this user is a member. - const userListMemberships = this.listUserMembershipsCache - .entries() - .filter(e => e[1].has(body.id)) - .map(e => e[0]) - .toArray(); - - if (isLocal) { - await Promise.all([ - this.userProfileCache.delete(body.id), - this.userMutingsCache.delete(body.id), - this.userBlockingCache.delete(body.id), - this.userBlockedCache.delete(body.id), - this.renoteMutingsCache.delete(body.id), - this.userFollowingsCache.delete(body.id), - this.userFollowersCache.delete(body.id), - this.hibernatedUserCache.delete(body.id), - this.threadMutingsCache.delete(body.id), - this.noteMutingsCache.delete(body.id), - this.userListMembershipsCache.delete(body.id), - this.listUserMembershipsCache.deleteMany(userListMemberships), - ]); - } } else { this.userByIdCache.set(user.id, user); for (const [k, v] of this.uriPersonCache.entries) { @@ -665,6 +711,7 @@ export class CacheService implements OnApplicationShutdown { @bindThis public dispose(): void { + this.internalEventService.off('usersUpdated', this.onBulkUserEvent); this.internalEventService.off('userChangeSuspendedState', this.onUserEvent); this.internalEventService.off('userChangeDeletedState', this.onUserEvent); this.internalEventService.off('remoteUserUpdated', this.onUserEvent); diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 658621d854..167d226832 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -237,6 +237,7 @@ export interface InternalEventTypes { userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; remoteUserUpdated: { id: MiUser['id']; }; localUserUpdated: { id: MiUser['id']; }; + usersUpdated: { ids: MiUser['id'][]; }; follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };