From 23995b3888325f5384f435d6e72c09d632167e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 1 Mar 2026 18:21:35 +0100 Subject: [PATCH] Nicolium/pl-api: an attempt at performance improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../queries/accounts/use-follow-requests.ts | 7 +-- packages/nicolium/src/queries/chats.ts | 14 +++--- .../statuses/use-interaction-requests.ts | 15 ++---- .../make-paginated-response-query-options.ts | 11 +---- .../utils/make-paginated-response-query.ts | 25 +++++----- .../nicolium/src/queries/utils/minify-list.ts | 46 +++++++++---------- packages/pl-api/lib/client-base.ts | 18 ++++---- packages/pl-api/lib/client/admin.ts | 28 +++++------ .../lib/client/grouped-notifications.ts | 13 +++--- packages/pl-api/lib/client/my-account.ts | 7 ++- packages/pl-api/lib/responses.ts | 41 ++++++++++++++--- .../notifications/use-notification-list.ts | 10 +--- 12 files changed, 118 insertions(+), 117 deletions(-) diff --git a/packages/nicolium/src/queries/accounts/use-follow-requests.ts b/packages/nicolium/src/queries/accounts/use-follow-requests.ts index 60a11aeaa..666d568d9 100644 --- a/packages/nicolium/src/queries/accounts/use-follow-requests.ts +++ b/packages/nicolium/src/queries/accounts/use-follow-requests.ts @@ -77,12 +77,7 @@ const prefetchFollowRequests = (client: PlApiClient) => queryKey: queryKeys.accountsLists.followRequests, queryFn: ({ pageParam }) => pageParam.next?.() ?? client.myAccount.getFollowRequests().then(minifyAccountList), - initialPageParam: { - previous: null, - next: null, - items: [], - partial: false, - } as PaginatedResponse, + initialPageParam: { next: null as (() => Promise>) | null }, }); export { diff --git a/packages/nicolium/src/queries/chats.ts b/packages/nicolium/src/queries/chats.ts index 5967edf0e..468653788 100644 --- a/packages/nicolium/src/queries/chats.ts +++ b/packages/nicolium/src/queries/chats.ts @@ -9,7 +9,7 @@ import sumBy from 'lodash/sumBy'; import { type Chat, type ChatMessage as BaseChatMessage, - type PaginatedResponse, + PaginatedResponse, chatMessageSchema, } from 'pl-api'; import * as v from 'valibot'; @@ -44,12 +44,12 @@ const normalizeChatMessagesList = ({ next, items, ...response -}: PaginatedResponse): PaginatedResponse => ({ - ...response, - previous: previous ? () => previous().then((res) => normalizeChatMessagesList(res)) : null, - next: next ? () => next().then((res) => normalizeChatMessagesList(res)) : null, - items: items.map(normalizeChatMessage), -}); +}: PaginatedResponse): PaginatedResponse => + new PaginatedResponse(items.map(normalizeChatMessage), { + ...response, + previous: previous ? () => previous().then((res) => normalizeChatMessagesList(res)) : null, + next: next ? () => next().then((res) => normalizeChatMessagesList(res)) : null, + }); const useChatMessages = (chat: Chat) => { const client = useClient(); diff --git a/packages/nicolium/src/queries/statuses/use-interaction-requests.ts b/packages/nicolium/src/queries/statuses/use-interaction-requests.ts index cbc273342..4d3176119 100644 --- a/packages/nicolium/src/queries/statuses/use-interaction-requests.ts +++ b/packages/nicolium/src/queries/statuses/use-interaction-requests.ts @@ -1,4 +1,5 @@ import { type InfiniteData, useInfiniteQuery, useMutation } from '@tanstack/react-query'; +import { type InteractionRequest, PaginatedResponse } from 'pl-api'; import { importEntities } from '@/actions/importer'; import { useClient } from '@/hooks/use-client'; @@ -7,8 +8,6 @@ import { useLoggedIn } from '@/hooks/use-logged-in'; import { queryKeys } from '../keys'; -import type { InteractionRequest, PaginatedResponse } from 'pl-api'; - const minifyInteractionRequest = ({ account, status, @@ -31,12 +30,11 @@ const minifyInteractionRequestsList = ({ }: PaginatedResponse): PaginatedResponse => { importEntities({ statuses: items.flatMap((item) => [item.status, item.reply]) }); - return { + return new PaginatedResponse(items.map(minifyInteractionRequest), { ...response, previous: previous ? () => previous().then(minifyInteractionRequestsList) : null, next: next ? () => next().then(minifyInteractionRequestsList) : null, - items: items.map(minifyInteractionRequest), - }; + }); }; const useInteractionRequests = ( @@ -52,11 +50,8 @@ const useInteractionRequests = ( pageParam.next?.() ?? client.interactionRequests.getInteractionRequests().then(minifyInteractionRequestsList), initialPageParam: { - previous: null, - next: null, - items: [], - partial: false, - } as PaginatedResponse, + next: null as (() => Promise>) | null, + }, getNextPageParam: (page) => (page.next ? page : undefined), enabled: isLoggedIn && features.interactionRequests, select, diff --git a/packages/nicolium/src/queries/utils/make-paginated-response-query-options.ts b/packages/nicolium/src/queries/utils/make-paginated-response-query-options.ts index 32bc55c26..f989761c4 100644 --- a/packages/nicolium/src/queries/utils/make-paginated-response-query-options.ts +++ b/packages/nicolium/src/queries/utils/make-paginated-response-query-options.ts @@ -31,12 +31,7 @@ const makePaginatedResponseQueryOptions = infiniteQueryOptions({ queryKey: typeof queryKey === 'object' ? queryKey : queryKey(...params), queryFn: ({ pageParam }) => pageParam.next?.() ?? queryFn(getClient(), params), - initialPageParam: { - previous: null, - next: null, - items: [] as unknown as PaginatedResponse['items'], - partial: false, - } as Awaited>, + initialPageParam: { next: null as (() => Promise>) | null }, getNextPageParam: (page) => (page.next ? page : undefined), select: select ?? @@ -49,9 +44,7 @@ const makePaginatedResponseQueryOptions = if (Array.isArray(lastPage.items)) { const items = PaginatedResponseArray.from( - data.pages.flatMap((page) => - Array.isArray(page.items) ? page.items : [page.items], - ), + data.pages.flatMap((page) => (Array.isArray(page.items) ? page.items : [page.items])), ).setMeta(lastPage.total, lastPage.partial); return items as T3; diff --git a/packages/nicolium/src/queries/utils/make-paginated-response-query.ts b/packages/nicolium/src/queries/utils/make-paginated-response-query.ts index 07d87b9d3..1ccf1352a 100644 --- a/packages/nicolium/src/queries/utils/make-paginated-response-query.ts +++ b/packages/nicolium/src/queries/utils/make-paginated-response-query.ts @@ -24,8 +24,18 @@ class PaginatedResponseArray extends Array { /** Set metadata as non-enumerable to preserve TanStack Query structural sharing. */ setMeta(total: number | undefined, partial: boolean | undefined): this { - Object.defineProperty(this, 'total', { value: total, writable: true, enumerable: false, configurable: true }); - Object.defineProperty(this, 'partial', { value: partial, writable: true, enumerable: false, configurable: true }); + Object.defineProperty(this, 'total', { + value: total, + writable: true, + enumerable: false, + configurable: true, + }); + Object.defineProperty(this, 'partial', { + value: partial, + writable: true, + enumerable: false, + configurable: true, + }); return this; } } @@ -57,12 +67,7 @@ const makePaginatedResponseQuery = return useInfiniteQuery({ queryKey: typeof queryKey === 'object' ? queryKey : queryKey(...params), queryFn: ({ pageParam }) => pageParam.next?.() ?? queryFn(client, params), - initialPageParam: { - previous: null, - next: null, - items: [] as unknown as PaginatedResponse['items'], - partial: false, - } as Awaited>, + initialPageParam: { next: null as (() => Promise>) | null }, getNextPageParam: (page) => (page.next ? page : undefined), select: select ?? @@ -75,9 +80,7 @@ const makePaginatedResponseQuery = if (Array.isArray(lastPage.items)) { const items = PaginatedResponseArray.from( - data.pages.flatMap((page) => - Array.isArray(page.items) ? page.items : [page.items], - ), + data.pages.flatMap((page) => (Array.isArray(page.items) ? page.items : [page.items])), ).setMeta(lastPage.total, lastPage.partial); return items as T3; diff --git a/packages/nicolium/src/queries/utils/minify-list.ts b/packages/nicolium/src/queries/utils/minify-list.ts index fcbd12a1b..e49ff340d 100644 --- a/packages/nicolium/src/queries/utils/minify-list.ts +++ b/packages/nicolium/src/queries/utils/minify-list.ts @@ -1,24 +1,23 @@ import { notifyManager } from '@tanstack/react-query'; +import { + PaginatedResponse, + type Account, + type AdminAccount, + type AdminReport, + type BlockedAccount, + type Conversation, + type Group, + type GroupedNotificationsResults, + type MutedAccount, + type NotificationGroup, + type Status, +} from 'pl-api'; import { importEntities } from '@/actions/importer'; import { queryClient } from '../client'; import { queryKeys } from '../keys'; -import type { - Account, - AdminAccount, - AdminReport, - BlockedAccount, - Conversation, - Group, - GroupedNotificationsResults, - MutedAccount, - NotificationGroup, - PaginatedResponse, - Status, -} from 'pl-api'; - const minifyList = ( { previous, next, items, ...response }: PaginatedResponse, minifier: (value: T1) => T2, @@ -31,7 +30,7 @@ const minifyList = ( isArray ? (items as T1[]).map(minifier) : minifier(items as T1) ) as PaginatedResponse['items']; - return { + return new PaginatedResponse(minifiedItems, { ...response, previous: previous ? () => @@ -40,8 +39,7 @@ const minifyList = ( next: next ? () => next().then((list) => minifyList(list, minifier, importer, isArray)) : null, - items: minifiedItems, - }; + }); }; const minifyStatusList = (response: PaginatedResponse): PaginatedResponse => @@ -188,14 +186,14 @@ const minifyAdminReport = ({ statuses, ...adminReport }: AdminReport) => { - minifyAdminAccountList({ - items: [account, action_taken_by_account, assigned_account, target_account].filter( - (a): a is AdminAccount => !!a, + minifyAdminAccountList( + new PaginatedResponse( + [account, action_taken_by_account, assigned_account, target_account].filter( + (a): a is AdminAccount => !!a, + ), + { partial: false }, ), - previous: null, - next: null, - partial: false, - }); + ); importEntities({ accounts: [ diff --git a/packages/pl-api/lib/client-base.ts b/packages/pl-api/lib/client-base.ts index f588c9473..174e36173 100644 --- a/packages/pl-api/lib/client-base.ts +++ b/packages/pl-api/lib/client-base.ts @@ -4,12 +4,12 @@ import { instanceSchema } from './entities/instance'; import { filteredArray } from './entities/utils'; import { type Features, getFeatures } from './features'; import request, { getNextLink, getPrevLink, type RequestBody } from './request'; +import { PaginatedResponse } from './responses'; import type { Instance } from './entities/instance'; import type { StreamingEvent } from './entities/streaming-event'; import type { StreamingParams } from './params/streaming'; import type { Response as PlApiResponse } from './request'; -import type { PaginatedResponse } from './responses'; interface PlApiClientConstructorOpts { /** Instance object to use by default, to be populated eg. from cache */ @@ -65,16 +65,18 @@ class PlApiBaseClient { body: RequestBody, schema: v.BaseSchema>, isArray = true as IsArray, - ): Promise> => { + ) => { const targetSchema = isArray ? filteredArray(schema) : schema; const processResponse = (response: PlApiResponse) => - ({ - previous: getMore(getPrevLink(response)), - next: getMore(getNextLink(response)), - items: v.parse(targetSchema, response.json), - partial: response.status === 206, - }) as PaginatedResponse; + new PaginatedResponse( + v.parse(targetSchema, response.json) as IsArray extends true ? Array : T, + { + previous: getMore(getPrevLink(response)), + next: getMore(getNextLink(response)), + partial: response.status === 206, + }, + ); const getMore = (input: string | null) => input ? () => this.request(input).then(processResponse) : null; diff --git a/packages/pl-api/lib/client/admin.ts b/packages/pl-api/lib/client/admin.ts index bf3d8edaf..96022bcad 100644 --- a/packages/pl-api/lib/client/admin.ts +++ b/packages/pl-api/lib/client/admin.ts @@ -26,8 +26,8 @@ import { import { filteredArray } from '@/entities/utils'; import { GOTOSOCIAL, MITRA, PLEROMA } from '../features'; +import { PaginatedResponse } from '../responses'; -import type { PaginatedResponse } from '../responses'; import type { PlApiBaseClient } from '@/client-base'; import type { AdminAccount, @@ -89,7 +89,7 @@ const paginatedPleromaAccounts = async ( const adminAccounts = v.parse(filteredArray(adminAccountSchema), response.json?.users); - return { + return new PaginatedResponse(adminAccounts, { previous: params.page ? () => paginatedPleromaAccounts(client, { ...params, page: params.page! - 1 }) : null, @@ -98,10 +98,9 @@ const paginatedPleromaAccounts = async ( params.page_size * ((params.page || 1) - 1) + response.json?.users?.length ? () => paginatedPleromaAccounts(client, { ...params, page: (params.page || 0) + 1 }) : null, - items: adminAccounts, partial: response.status === 206, total: response.json?.count, - }; + }); }; const paginatedPleromaReports = async ( @@ -115,7 +114,7 @@ const paginatedPleromaReports = async ( ): Promise> => { const response = await client.request('/api/v1/pleroma/admin/reports', { params }); - return { + return new PaginatedResponse(v.parse(filteredArray(adminReportSchema), response.json?.reports), { previous: params.page ? () => paginatedPleromaReports(client, { ...params, page: params.page! - 1 }) : null, @@ -124,10 +123,9 @@ const paginatedPleromaReports = async ( params.page_size * ((params.page || 1) - 1) + response.json?.reports?.length ? () => paginatedPleromaReports(client, { ...params, page: (params.page || 0) + 1 }) : null, - items: v.parse(filteredArray(adminReportSchema), response.json?.reports), partial: response.status === 206, total: response.json?.total, - }; + }); }; const paginatedPleromaStatuses = async ( @@ -142,16 +140,15 @@ const paginatedPleromaStatuses = async ( ): Promise> => { const response = await client.request('/api/v1/pleroma/admin/statuses', { params }); - return { + return new PaginatedResponse(v.parse(filteredArray(statusSchema), response.json), { previous: params.page ? () => paginatedPleromaStatuses(client, { ...params, page: params.page! - 1 }) : null, next: response.json?.length ? () => paginatedPleromaStatuses(client, { ...params, page: (params.page || 0) + 1 }) : null, - items: v.parse(filteredArray(statusSchema), response.json), partial: response.status === 206, - }; + }); }; const admin = (client: PlApiBaseClient) => { @@ -1189,8 +1186,7 @@ const admin = (client: PlApiBaseClient) => { const items = v.parse(filteredArray(adminAnnouncementSchema), response.json); - return { - previous: null, + return new PaginatedResponse(items, { next: items.length ? () => category.announcements.getAnnouncements({ @@ -1198,9 +1194,8 @@ const admin = (client: PlApiBaseClient) => { offset: (params?.offset || 0) + items.length, }) : null, - items, partial: false, - }; + }); }, /** @@ -1336,7 +1331,7 @@ const admin = (client: PlApiBaseClient) => { const items = v.parse(filteredArray(adminModerationLogEntrySchema), response.json.items); - return { + return new PaginatedResponse(items, { previous: params.page && params.page > 1 ? () => category.moderationLog.getModerationLog({ ...params, page: params.page! - 1 }) @@ -1349,9 +1344,8 @@ const admin = (client: PlApiBaseClient) => { page: (params.page || 1) + 1, }) : null, - items, partial: response.status === 206, - }; + }); }, }, diff --git a/packages/pl-api/lib/client/grouped-notifications.ts b/packages/pl-api/lib/client/grouped-notifications.ts index 222abb8af..0b3dcb5c1 100644 --- a/packages/pl-api/lib/client/grouped-notifications.ts +++ b/packages/pl-api/lib/client/grouped-notifications.ts @@ -4,9 +4,9 @@ import { accountSchema, groupedNotificationsResultsSchema } from '@/entities'; import { filteredArray } from '@/entities/utils'; import { type RequestMeta } from '../request'; +import { PaginatedResponse } from '../responses'; import { pick, omit } from '../utils'; -import type { PaginatedResponse } from '../responses'; import type { notifications } from './notifications'; import type { PlApiBaseClient } from '@/client-base'; import type { @@ -158,12 +158,11 @@ const groupedNotifications = ( const response = await client.request(`/api/v1/notifications/${groupKey}`); - return groupNotifications({ - previous: null, - next: null, - items: [response.json], - partial: false, - }).items; + return groupNotifications( + new PaginatedResponse([response.json], { + partial: false, + }), + ).items; }, /** diff --git a/packages/pl-api/lib/client/my-account.ts b/packages/pl-api/lib/client/my-account.ts index 12eb976dd..c8b2ceb04 100644 --- a/packages/pl-api/lib/client/my-account.ts +++ b/packages/pl-api/lib/client/my-account.ts @@ -13,8 +13,8 @@ import { filteredArray } from '@/entities/utils'; import { GOTOSOCIAL, ICESHRIMP_NET, MITRA, PIXELFED, PLEROMA } from '../features'; import { getNextLink, getPrevLink } from '../request'; +import { PaginatedResponse } from '../responses'; -import type { PaginatedResponse } from '../responses'; import type { accounts } from './accounts'; import type { PlApiBaseClient } from '@/client-base'; import type { Account } from '@/entities'; @@ -44,12 +44,11 @@ const paginatedIceshrimpAccountsList = async ( const prevLink = getPrevLink(response); const nextLink = getNextLink(response); - return { + return new PaginatedResponse(items, { previous: prevLink ? () => paginatedIceshrimpAccountsList(client, prevLink, fn) : null, next: nextLink ? () => paginatedIceshrimpAccountsList(client, nextLink, fn) : null, - items, partial: response.status === 206, - }; + }); }; const myAccount = (client: PlApiBaseClient & { accounts: ReturnType }) => ({ diff --git a/packages/pl-api/lib/responses.ts b/packages/pl-api/lib/responses.ts index 361e838c5..87807dabf 100644 --- a/packages/pl-api/lib/responses.ts +++ b/packages/pl-api/lib/responses.ts @@ -1,12 +1,41 @@ /** * @category Utils */ -interface PaginatedResponse { - previous: (() => Promise>) | null; - next: (() => Promise>) | null; +class PaginatedResponse { items: IsArray extends true ? Array : T; - partial: boolean; - total?: number; + declare previous: (() => Promise>) | null; + declare next: (() => Promise>) | null; + declare total: number | undefined; + declare partial: boolean; + + constructor( + items: IsArray extends true ? Array : T, + info: Partial, 'items'>> = {}, + ) { + this.items = items; + Object.defineProperties(this, { + items: { value: items, writable: true, enumerable: true, configurable: true }, + previous: { + value: info.previous ?? null, + writable: true, + enumerable: false, + configurable: true, + }, + next: { + value: info.next ?? null, + writable: true, + enumerable: false, + configurable: true, + }, + total: { value: info.total, writable: true, enumerable: false, configurable: true }, + partial: { + value: info.partial ?? false, + writable: true, + enumerable: false, + configurable: true, + }, + }); + } } -export type { PaginatedResponse }; +export { PaginatedResponse }; diff --git a/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts b/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts index a300cc73d..e9de87aca 100644 --- a/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts +++ b/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts @@ -67,10 +67,7 @@ const useNotificationList = ( exclude_types: params.excludeTypes, }) ).then(importNotifications), - initialPageParam: { previous: null, next: null } as Pick< - PaginatedResponse, - 'previous' | 'next' - >, + initialPageParam: { next: null as (() => Promise>) | null }, getNextPageParam: (response) => response, }, queryClient, @@ -94,10 +91,7 @@ const prefetchNotifications = (client: PlApiClient, params: UseNotificationParam exclude_types: params.excludeTypes, }) .then(importNotifications), - initialPageParam: { previous: null, next: null } as Pick< - PaginatedResponse, - 'previous' | 'next' - >, + initialPageParam: { next: null as (() => Promise>) | null }, }); export { useNotificationList, prefetchNotifications };