fix: eliminate duplicate API requests by deduplicating user fetch calls

- Add request deduplication to getOrFetchUser using RequestDeduplicator
- Consolidates multiple simultaneous calls for same user into single API request
- Prevents duplicate 401 errors and wasted network traffic
- Matches pattern already used by fetchUsers and fetchUserStats
- Remove backwards compatibility aliases (getOrFetchUserByName, getOrFetchUserByUsername)
- Update all call sites to use canonical getOrFetchUser method

Fixes issue where PagedMemoList, useMemoViewState, MainLayout, and UserProfile
were making duplicate user fetch requests when loading user data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven 2025-12-22 20:12:44 +08:00
parent 5828f34aae
commit be7ef74698
7 changed files with 23 additions and 40 deletions

View File

@ -50,7 +50,7 @@ const MemoCommentMessage = observer(({ notification }: Props) => {
});
setCommentMemo(comment);
const sender = await userStore.getOrFetchUserByName(notification.sender);
const sender = await userStore.getOrFetchUser(notification.sender);
setSender(sender);
setInitialized(true);
}

View File

@ -15,7 +15,7 @@ export const useReactionGroups = (reactions: Reaction[]): ReactionGroup => {
const fetchReactionGroups = async () => {
const newReactionGroup = new Map<string, User[]>();
for (const reaction of reactions) {
const user = await userStore.getOrFetchUserByName(reaction.creator);
const user = await userStore.getOrFetchUser(reaction.creator);
const users = newReactionGroup.get(reaction.reactionType) || [];
users.push(user);
newReactionGroup.set(reaction.reactionType, uniq(users));

View File

@ -116,7 +116,7 @@ export const useMemoCreator = (creatorName: string) => {
if (fetchedRef.current) return;
fetchedRef.current = true;
(async () => {
const user = await userStore.getOrFetchUserByName(creatorName);
const user = await userStore.getOrFetchUser(creatorName);
setCreator(user);
})();
}, [creatorName]);

View File

@ -67,7 +67,7 @@ const PagedMemoList = observer((props: Props) => {
// This significantly improves perceived performance by pre-populating the cache
if (response?.memos && props.showCreator) {
const uniqueCreators = Array.from(new Set(response.memos.map((memo) => memo.creator)));
await Promise.allSettled(uniqueCreators.map((creator) => userStore.getOrFetchUserByName(creator)));
await Promise.allSettled(uniqueCreators.map((creator) => userStore.getOrFetchUser(creator)));
}
} finally {
setIsRequesting(false);

View File

@ -35,7 +35,7 @@ const MainLayout = observer(() => {
// Fetch or get user to obtain user name (e.g., "users/123")
// Note: User stats will be fetched by useFilteredMemoStats
userStore
.getOrFetchUserByUsername(username)
.getOrFetchUser(`users/${username}`)
.then((user) => {
setProfileUserName(user.name);
})

View File

@ -30,7 +30,7 @@ const UserProfile = observer(() => {
}
userStore
.getOrFetchUserByUsername(username)
.getOrFetchUser(`users/${username}`)
.then((user) => {
setUser(user);
loadingState.setFinish();

View File

@ -72,44 +72,28 @@ const userStore = (() => {
const state = new LocalState();
const deduplicator = new RequestDeduplicator();
const getOrFetchUserByName = async (name: string) => {
const getOrFetchUser = async (name: string) => {
const userMap = state.userMapByName;
if (userMap[name]) {
return userMap[name] as User;
}
const user = await userServiceClient.getUser({
name: name,
});
state.setPartial({
userMapByName: {
...userMap,
[name]: user,
},
});
return user;
};
const getOrFetchUserByUsername = async (username: string) => {
const userMap = state.userMapByName;
for (const name in userMap) {
if (userMap[name].username === username) {
return userMap[name];
const requestKey = createRequestKey("getOrFetchUser", { name });
return deduplicator.execute(requestKey, async () => {
// Double-check cache in case another request finished first
if (state.userMapByName[name]) {
return state.userMapByName[name] as User;
}
}
// Use GetUser with username - supports both "users/{id}" and "users/{username}"
const user = await userServiceClient.getUser({
name: `users/${username}`,
const user = await userServiceClient.getUser({
name: name,
});
state.setPartial({
userMapByName: {
...state.userMapByName,
[name]: user,
},
});
return user;
});
if (!user) {
throw new Error(`User with username ${username} not found`);
}
state.setPartial({
userMapByName: {
...userMap,
[user.name]: user,
},
});
return user;
};
const getUserByName = (name: string) => {
@ -292,8 +276,7 @@ const userStore = (() => {
return {
state,
getOrFetchUserByName,
getOrFetchUserByUsername,
getOrFetchUser,
getUserByName,
fetchUsers,
updateUser,