pl-fe: batch relationships requests

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-10-23 19:47:13 +02:00
parent 2effae78de
commit 72d86fd62d
8 changed files with 41 additions and 128 deletions

View File

@ -71,6 +71,7 @@
"@twemoji/svg": "^15.0.0",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^4.3.4",
"@yornaath/batshit": "^0.11.1",
"abortcontroller-polyfill": "^1.7.8",
"autoprefixer": "^10.4.21",
"blurhash": "^2.0.5",

View File

@ -1,5 +1,6 @@
import { type CreateAccountParams, type Relationship } from 'pl-api';
import { batcher } from 'pl-fe/api/batcher';
import { queryClient } from 'pl-fe/queries/client';
import { selectAccount } from 'pl-fe/selectors';
import { isLoggedIn } from 'pl-fe/utils/auth';
@ -85,7 +86,9 @@ const fetchRelationships = (accountIds: string[]) =>
return null;
}
return getClient(getState()).accounts.getRelationships(newAccountIds)
const fetcher = batcher.relationships(getClient(getState())).fetch;
return Promise.all(newAccountIds.map(fetcher))
.then(response => dispatch(importEntities({ relationships: response })));
};

View File

@ -0,0 +1,16 @@
import { bufferScheduler, create, keyResolver } from '@yornaath/batshit';
import memoize from 'lodash/memoize';
import type { PlApiClient } from 'pl-api';
const relationships = memoize((client: PlApiClient) => create({
fetcher: (ids: string[]) => client.accounts.getRelationships(ids),
resolver: keyResolver('id'),
scheduler: bufferScheduler(200),
}));
const batcher = {
relationships,
};
export { batcher };

View File

@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, type AccountsAction } from 'pl-fe/actions/accounts';
import { batcher } from 'pl-fe/api/batcher';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useClient } from 'pl-fe/hooks/use-client';
import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
@ -34,7 +35,7 @@ const useRelationshipQuery = (accountId?: string) => {
return useQuery({
queryKey: ['accountRelationships', accountId],
queryFn: () => client.accounts.getRelationships([accountId!]).then(arr => arr[0]),
queryFn: () => batcher.relationships(client).fetch(accountId!).then((data) => data || undefined),
enabled: isLoggedIn && !!accountId,
});
};

View File

@ -4,6 +4,7 @@ import { type Chat, type ChatMessage as BaseChatMessage, type PaginatedResponse,
import * as v from 'valibot';
import { importEntities } from 'pl-fe/actions/importer';
import { batcher } from 'pl-fe/api/batcher';
import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context';
import { useStatContext } from 'pl-fe/contexts/stat-context';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
@ -17,7 +18,6 @@ import { flattenPages, updatePageItem } from 'pl-fe/utils/queries';
import { useRelationshipQuery } from './accounts/use-relationship';
import { queryClient } from './client';
import { useFetchRelationships } from './relationships';
const ChatKeys = {
chat: (chatId?: string) => ['chats', 'chat', chatId] as const,
@ -60,7 +60,6 @@ const useChats = () => {
const dispatch = useAppDispatch();
const features = useFeatures();
const { setUnreadChatsCount } = useStatContext();
const fetchRelationships = useFetchRelationships();
const { me } = useLoggedIn();
const getChats = async (pageParam?: Pick<PaginatedResponse<Chat>, 'next'>): Promise<PaginatedResponse<Chat>> => {
@ -70,7 +69,8 @@ const useChats = () => {
setUnreadChatsCount(sumBy(data, (chat) => chat.unread));
// Set the relationships to these users in the redux store.
fetchRelationships.mutate({ accountIds: items.map((item) => item.account.id) });
const fetcher = batcher.relationships(client).fetch;
items.map((item) => item.account.id).forEach(fetcher);
dispatch(importEntities({ accounts: items.map((item) => item.account) }));
return response;
@ -101,13 +101,11 @@ const useChats = () => {
const useChat = (chatId?: string) => {
const client = useClient();
const dispatch = useAppDispatch();
const fetchRelationships = useFetchRelationships();
const getChat = async () => {
if (chatId) {
const data = await client.chats.getChat(chatId);
fetchRelationships.mutate({ accountIds: [data.account.id] });
dispatch(importEntities({ accounts: [data.account] }));
return data;

View File

@ -1,107 +0,0 @@
import { useEffect } from 'react';
import { __stub } from 'pl-fe/api';
import { buildRelationship } from 'pl-fe/jest/factory';
import { createTestStore, queryClient, renderHook, rootState, waitFor } from 'pl-fe/jest/test-helpers';
import { Store } from 'pl-fe/store';
import { useFetchRelationships } from './relationships';
describe('useFetchRelationships()', () => {
let store: Store;
beforeEach(() => {
const state = rootState;
store = createTestStore(state);
queryClient.clear();
});
describe('with a successful query', () => {
describe('with one relationship', () => {
const id = '123';
beforeEach(() => {
__stub((mock) => {
mock
.onGet(`/api/v1/accounts/relationships?id[]=${id}`)
.reply(200, [buildRelationship({ id, blocked_by: true })]);
});
});
it('is successful', async() => {
renderHook(() => {
const fetchRelationships = useFetchRelationships();
useEffect(() => {
fetchRelationships.mutate({ accountIds: [id] });
}, []);
return fetchRelationships;
}, undefined, store);
await waitFor(() => {
expect(store.getState().relationships.size).toBe(1);
expect(store.getState().relationships.getIn([id, 'id'])).toBe(id);
expect(store.getState().relationships.getIn([id, 'blocked_by'])).toBe(true);
});
});
});
describe('with multiple relationships', () => {
const ids = ['123', '456'];
beforeEach(() => {
__stub((mock) => {
mock
.onGet(`/api/v1/accounts/relationships?id[]=${ids[0]}&id[]=${ids[1]}`)
.reply(200, ids.map((id) => buildRelationship({ id, blocked_by: true })));
});
});
it('is successful', async() => {
renderHook(() => {
const fetchRelationships = useFetchRelationships();
useEffect(() => {
fetchRelationships.mutate({ accountIds: ids });
}, []);
return fetchRelationships;
}, undefined, store);
await waitFor(() => {
expect(store.getState().relationships.size).toBe(2);
expect(store.getState().relationships.getIn([ids[0], 'id'])).toBe(ids[0]);
expect(store.getState().relationships.getIn([ids[1], 'id'])).toBe(ids[1]);
});
});
});
});
describe('with an unsuccessful query', () => {
const id = '123';
beforeEach(() => {
__stub((mock) => {
mock.onGet(`/api/v1/accounts/relationships?id[]=${id}`).networkError();
});
});
it('is successful', async() => {
const { result } = renderHook(() => {
const fetchRelationships = useFetchRelationships();
useEffect(() => {
fetchRelationships.mutate({ accountIds: [id] });
}, []);
return fetchRelationships;
}, undefined, store);
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.error).toBeDefined();
});
});
});

View File

@ -1,14 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { useClient } from 'pl-fe/hooks/use-client';
const useFetchRelationships = () => {
const client = useClient();
return useMutation({
mutationFn: ({ accountIds }: { accountIds: string[]}) =>
client.accounts.getRelationships(accountIds),
});
};
export { useFetchRelationships };

15
pnpm-lock.yaml generated
View File

@ -214,6 +214,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.7.0(vite@5.4.21(@types/node@22.17.0)(sass@1.89.2)(terser@5.44.0))
'@yornaath/batshit':
specifier: ^0.11.1
version: 0.11.1
abortcontroller-polyfill:
specifier: ^1.7.8
version: 1.7.8
@ -2899,6 +2902,12 @@ packages:
'@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
'@yornaath/batshit-devtools@1.7.1':
resolution: {integrity: sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ==}
'@yornaath/batshit@0.11.1':
resolution: {integrity: sha512-LYsrHbsdGivKIr2IDcwaqrEYoW2W27a68GT4eW1jlM7nHO3KtH2qItqaOD57wiiOpYzxFRTVz7LUZMVMSfaekQ==}
abortcontroller-polyfill@1.7.8:
resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==}
@ -9405,6 +9414,12 @@ snapshots:
'@xtuc/long@4.2.2': {}
'@yornaath/batshit-devtools@1.7.1': {}
'@yornaath/batshit@0.11.1':
dependencies:
'@yornaath/batshit-devtools': 1.7.1
abortcontroller-polyfill@1.7.8: {}
acorn-import-phases@1.0.4(acorn@8.15.0):