mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
fix(user): stop invalidating user cache on profile reads (#2142)
This commit is contained in:
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]]);
|
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 () => {
|
it('invalidates the users service cache when seeding a partial from a user update', async () => {
|
||||||
const userId = createUserID(4001n);
|
const userId = createUserID(4001n);
|
||||||
const usersServiceClient = new FakeUsersServiceClient();
|
const usersServiceClient = new FakeUsersServiceClient();
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ export class UserCacheService {
|
|||||||
return response;
|
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 {
|
private createDeletedUserPartialFallback(userId: UserID): UserPartialResponse {
|
||||||
return {
|
return {
|
||||||
id: userId.toString(),
|
id: userId.toString(),
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ export async function getCachedUserPartialResponses(params: {
|
|||||||
return await userCacheService.getUserPartialResponses(userIds, requestCache);
|
return await userCacheService.getUserPartialResponses(userIds, requestCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mapUserToPartialResponseWithCache(params: {
|
export function mapUserToPartialResponseWithCache(params: {
|
||||||
user: User;
|
user: User;
|
||||||
userCacheService: Pick<UserCacheService, 'setUserPartialResponseFromUserInBackground'>;
|
userCacheService: Pick<UserCacheService, 'setUserPartialResponseFromUserInRequestCache'>;
|
||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<UserPartialResponse> {
|
}): UserPartialResponse {
|
||||||
const {user, userCacheService, requestCache} = params;
|
const {user, userCacheService, requestCache} = params;
|
||||||
const cached = requestCache.userPartials.get(user.id);
|
const cached = requestCache.userPartials.get(user.id);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
return userCacheService.setUserPartialResponseFromUserInBackground(user, requestCache);
|
return userCacheService.setUserPartialResponseFromUserInRequestCache(user, requestCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function invalidateUserCache(params: {
|
export async function invalidateUserCache(params: {
|
||||||
|
|||||||
@@ -479,19 +479,17 @@ export class UserAccountRequestService {
|
|||||||
const guildMemberProfile = mapGuildMemberToProfileResponse(profile.guildMemberDomain ?? null, {restrictProfile});
|
const guildMemberProfile = mapGuildMemberToProfileResponse(profile.guildMemberDomain ?? null, {restrictProfile});
|
||||||
const timezoneOffset = profile.timezoneVisible ? getCurrentTimeZoneOffsetMinutes(profileUser.timezone) : null;
|
const timezoneOffset = profile.timezoneVisible ? getCurrentTimeZoneOffsetMinutes(profileUser.timezone) : null;
|
||||||
const mutualFriends = profile.mutualFriends
|
const mutualFriends = profile.mutualFriends
|
||||||
? await Promise.all(
|
? profile.mutualFriends.map((user) =>
|
||||||
profile.mutualFriends.map((user) =>
|
mapUserToPartialResponseWithCache({
|
||||||
mapUserToPartialResponseWithCache({
|
user,
|
||||||
user,
|
userCacheService: this.userCacheService,
|
||||||
userCacheService: this.userCacheService,
|
requestCache: params.requestCache,
|
||||||
requestCache: params.requestCache,
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
const connectedAccounts = profile.connections ? this.mapConnectionsToResponse(profile.connections) : undefined;
|
const connectedAccounts = profile.connections ? this.mapConnectionsToResponse(profile.connections) : undefined;
|
||||||
return {
|
return {
|
||||||
user: await mapUserToPartialResponseWithCache({
|
user: mapUserToPartialResponseWithCache({
|
||||||
user: profileUser,
|
user: profileUser,
|
||||||
userCacheService: this.userCacheService,
|
userCacheService: this.userCacheService,
|
||||||
requestCache: params.requestCache,
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user