fix(user): stop invalidating user cache on profile reads (#2142)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-30 22:16:14 +02:00
1 parent fcb629ca06
commit 6eca64a8f7
5 files changed
+76 -13

No files matched your search

@@ -136,6 +136,21 @@ describe('UserCacheService', () => {
expect(usersServiceClient.requests).toEqual([[userId]]);
});
it('seeds only the request cache when mapping a user on a read path', () => {
const userId = createUserID(4501n);
const usersServiceClient = new FakeUsersServiceClient();
const service = new UserCacheService(usersServiceClient);
const user = createUser(userId, 'ReadUser');
const requestCache = createRequestCache();
const partial = service.setUserPartialResponseFromUserInRequestCache(user, requestCache);
expect(partial.username).toBe('ReadUser');
expect(requestCache.userPartials.get(userId)).toBe(partial);
expect(usersServiceClient.invalidated).toEqual([]);
expect(usersServiceClient.requests).toEqual([]);
});
it('invalidates the users service cache when seeding a partial from a user update', async () => {
const userId = createUserID(4001n);
const usersServiceClient = new FakeUsersServiceClient();
@@ -94,6 +94,12 @@ export class UserCacheService {
return response;
}
setUserPartialResponseFromUserInRequestCache(user: User, requestCache: RequestCache): UserPartialResponse {
const response = mapUserToPartialResponse(user);
requestCache.userPartials.set(user.id, response);
return response;
}
private createDeletedUserPartialFallback(userId: UserID): UserPartialResponse {
return {
id: userId.toString(),
+4 -4
View File
@@ -24,17 +24,17 @@ export async function getCachedUserPartialResponses(params: {
return await userCacheService.getUserPartialResponses(userIds, requestCache);
}
export async function mapUserToPartialResponseWithCache(params: {
export function mapUserToPartialResponseWithCache(params: {
user: User;
userCacheService: Pick<UserCacheService, 'setUserPartialResponseFromUserInBackground'>;
userCacheService: Pick<UserCacheService, 'setUserPartialResponseFromUserInRequestCache'>;
requestCache: RequestCache;
}): Promise<UserPartialResponse> {
}): UserPartialResponse {
const {user, userCacheService, requestCache} = params;
const cached = requestCache.userPartials.get(user.id);
if (cached) {
return cached;
}
return userCacheService.setUserPartialResponseFromUserInBackground(user, requestCache);
return userCacheService.setUserPartialResponseFromUserInRequestCache(user, requestCache);
}
export async function invalidateUserCache(params: {
@@ -479,19 +479,17 @@ export class UserAccountRequestService {
const guildMemberProfile = mapGuildMemberToProfileResponse(profile.guildMemberDomain ?? null, {restrictProfile});
const timezoneOffset = profile.timezoneVisible ? getCurrentTimeZoneOffsetMinutes(profileUser.timezone) : null;
const mutualFriends = profile.mutualFriends
? await Promise.all(
profile.mutualFriends.map((user) =>
mapUserToPartialResponseWithCache({
user,
userCacheService: this.userCacheService,
requestCache: params.requestCache,
}),
),
? profile.mutualFriends.map((user) =>
mapUserToPartialResponseWithCache({
user,
userCacheService: this.userCacheService,
requestCache: params.requestCache,
}),
)
: undefined;
const connectedAccounts = profile.connections ? this.mapConnectionsToResponse(profile.connections) : undefined;
return {
user: await mapUserToPartialResponseWithCache({
user: mapUserToPartialResponseWithCache({
user: profileUser,
userCacheService: this.userCacheService,
requestCache: params.requestCache,
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {UserProfileFullResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
import {createFriendship} from '../../channel/tests/ChannelTestUtils';
import {UserCacheService} from '../../infrastructure/UserCacheService';
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
import {createBuilder} from '../../test/TestRequestBuilder';
describe('User Profile Cache Invalidation', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('resolves the profile subject and mutual friends without invalidating the users service cache', async () => {
const viewerAccount = await createTestAccount(harness);
const targetAccount = await createTestAccount(harness);
const mutualAccount = await createTestAccount(harness);
await createFriendship(harness, viewerAccount, targetAccount);
await createFriendship(harness, viewerAccount, mutualAccount);
await createFriendship(harness, targetAccount, mutualAccount);
const invalidateUserCache = vi.spyOn(UserCacheService.prototype, 'invalidateUserCache');
try {
const profile = await createBuilder<UserProfileFullResponse>(harness, viewerAccount.token)
.get(`/users/${targetAccount.userId}/profile?with_mutual_friends=true`)
.execute();
expect(profile.user.id).toBe(targetAccount.userId);
expect(profile.mutual_friends?.map((friend) => friend.id)).toEqual([mutualAccount.userId]);
expect(invalidateUserCache).not.toHaveBeenCalled();
} finally {
invalidateUserCache.mockRestore();
}
});
});