From 72d86fd62d24041e6f7ec140ae61334d768c7367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 23 Oct 2025 19:47:13 +0200 Subject: [PATCH] pl-fe: batch relationships requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/package.json | 1 + packages/pl-fe/src/actions/accounts.ts | 5 +- packages/pl-fe/src/api/batcher.ts | 16 +++ .../src/queries/accounts/use-relationship.ts | 3 +- packages/pl-fe/src/queries/chats.ts | 8 +- .../pl-fe/src/queries/relationships.test.ts | 107 ------------------ packages/pl-fe/src/queries/relationships.ts | 14 --- pnpm-lock.yaml | 15 +++ 8 files changed, 41 insertions(+), 128 deletions(-) create mode 100644 packages/pl-fe/src/api/batcher.ts delete mode 100644 packages/pl-fe/src/queries/relationships.test.ts delete mode 100644 packages/pl-fe/src/queries/relationships.ts diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 51c5da77d..e65f52c42 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -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", diff --git a/packages/pl-fe/src/actions/accounts.ts b/packages/pl-fe/src/actions/accounts.ts index 9b62d21fb..b187f71a5 100644 --- a/packages/pl-fe/src/actions/accounts.ts +++ b/packages/pl-fe/src/actions/accounts.ts @@ -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 }))); }; diff --git a/packages/pl-fe/src/api/batcher.ts b/packages/pl-fe/src/api/batcher.ts new file mode 100644 index 000000000..bff59a4ba --- /dev/null +++ b/packages/pl-fe/src/api/batcher.ts @@ -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 }; diff --git a/packages/pl-fe/src/queries/accounts/use-relationship.ts b/packages/pl-fe/src/queries/accounts/use-relationship.ts index 37efa8521..a4ffaf1de 100644 --- a/packages/pl-fe/src/queries/accounts/use-relationship.ts +++ b/packages/pl-fe/src/queries/accounts/use-relationship.ts @@ -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, }); }; diff --git a/packages/pl-fe/src/queries/chats.ts b/packages/pl-fe/src/queries/chats.ts index a379ec94a..98c51993d 100644 --- a/packages/pl-fe/src/queries/chats.ts +++ b/packages/pl-fe/src/queries/chats.ts @@ -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, 'next'>): Promise> => { @@ -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; diff --git a/packages/pl-fe/src/queries/relationships.test.ts b/packages/pl-fe/src/queries/relationships.test.ts deleted file mode 100644 index d2ffe174f..000000000 --- a/packages/pl-fe/src/queries/relationships.test.ts +++ /dev/null @@ -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(); - }); - }); -}); diff --git a/packages/pl-fe/src/queries/relationships.ts b/packages/pl-fe/src/queries/relationships.ts deleted file mode 100644 index 6185a0750..000000000 --- a/packages/pl-fe/src/queries/relationships.ts +++ /dev/null @@ -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 }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0113610a..b30fd84e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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):