Nicolium/pl-api: an attempt at performance improvements

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-01 18:21:35 +01:00
parent 46b12c5497
commit 23995b3888
12 changed files with 118 additions and 117 deletions

View File

@ -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<string>,
initialPageParam: { next: null as (() => Promise<PaginatedResponse<string>>) | null },
});
export {

View File

@ -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<BaseChatMessage>): PaginatedResponse<ChatMessage> => ({
...response,
previous: previous ? () => previous().then((res) => normalizeChatMessagesList(res)) : null,
next: next ? () => next().then((res) => normalizeChatMessagesList(res)) : null,
items: items.map(normalizeChatMessage),
});
}: PaginatedResponse<BaseChatMessage>): PaginatedResponse<ChatMessage> =>
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();

View File

@ -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<InteractionRequest>): PaginatedResponse<MinifiedInteractionRequest> => {
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 = <T>(
@ -52,11 +50,8 @@ const useInteractionRequests = <T>(
pageParam.next?.() ??
client.interactionRequests.getInteractionRequests().then(minifyInteractionRequestsList),
initialPageParam: {
previous: null,
next: null,
items: [],
partial: false,
} as PaginatedResponse<MinifiedInteractionRequest>,
next: null as (() => Promise<PaginatedResponse<MinifiedInteractionRequest>>) | null,
},
getNextPageParam: (page) => (page.next ? page : undefined),
enabled: isLoggedIn && features.interactionRequests,
select,

View File

@ -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<T2, IsArray>['items'],
partial: false,
} as Awaited<ReturnType<typeof queryFn>>,
initialPageParam: { next: null as (() => Promise<PaginatedResponse<T2, IsArray>>) | 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;

View File

@ -24,8 +24,18 @@ class PaginatedResponseArray<T> extends Array<T> {
/** 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<T2, IsArray>['items'],
partial: false,
} as Awaited<ReturnType<typeof queryFn>>,
initialPageParam: { next: null as (() => Promise<PaginatedResponse<T2, IsArray>>) | 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;

View File

@ -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 = <T1, T2, IsArray extends boolean = true>(
{ previous, next, items, ...response }: PaginatedResponse<T1, IsArray>,
minifier: (value: T1) => T2,
@ -31,7 +30,7 @@ const minifyList = <T1, T2, IsArray extends boolean = true>(
isArray ? (items as T1[]).map(minifier) : minifier(items as T1)
) as PaginatedResponse<T2, IsArray>['items'];
return {
return new PaginatedResponse(minifiedItems, {
...response,
previous: previous
? () =>
@ -40,8 +39,7 @@ const minifyList = <T1, T2, IsArray extends boolean = true>(
next: next
? () => next().then((list) => minifyList<T1, T2, IsArray>(list, minifier, importer, isArray))
: null,
items: minifiedItems,
};
});
};
const minifyStatusList = (response: PaginatedResponse<Status>): PaginatedResponse<string> =>
@ -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: [

View File

@ -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<any, T, v.BaseIssue<unknown>>,
isArray = true as IsArray,
): Promise<PaginatedResponse<T, typeof isArray>> => {
) => {
const targetSchema = isArray ? filteredArray(schema) : schema;
const processResponse = (response: PlApiResponse<any>) =>
({
previous: getMore(getPrevLink(response)),
next: getMore(getNextLink(response)),
items: v.parse(targetSchema, response.json),
partial: response.status === 206,
}) as PaginatedResponse<T, IsArray>;
new PaginatedResponse<T, IsArray>(
v.parse(targetSchema, response.json) as IsArray extends true ? Array<T> : 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;

View File

@ -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<PaginatedResponse<AdminReport>> => {
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<PaginatedResponse<Status>> => {
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,
};
});
},
},

View File

@ -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;
},
/**

View File

@ -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 <T>(
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<typeof accounts> }) => ({

View File

@ -1,12 +1,41 @@
/**
* @category Utils
*/
interface PaginatedResponse<T, IsArray extends boolean = true> {
previous: (() => Promise<PaginatedResponse<T, IsArray>>) | null;
next: (() => Promise<PaginatedResponse<T, IsArray>>) | null;
class PaginatedResponse<T, IsArray extends boolean = true> {
items: IsArray extends true ? Array<T> : T;
partial: boolean;
total?: number;
declare previous: (() => Promise<PaginatedResponse<T, IsArray>>) | null;
declare next: (() => Promise<PaginatedResponse<T, IsArray>>) | null;
declare total: number | undefined;
declare partial: boolean;
constructor(
items: IsArray extends true ? Array<T> : T,
info: Partial<Omit<PaginatedResponse<T, IsArray>, '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 };

View File

@ -67,10 +67,7 @@ const useNotificationList = (
exclude_types: params.excludeTypes,
})
).then(importNotifications),
initialPageParam: { previous: null, next: null } as Pick<
PaginatedResponse<BaseNotification>,
'previous' | 'next'
>,
initialPageParam: { next: null as (() => Promise<PaginatedResponse<BaseNotification>>) | 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<BaseNotification>,
'previous' | 'next'
>,
initialPageParam: { next: null as (() => Promise<PaginatedResponse<BaseNotification>>) | null },
});
export { useNotificationList, prefetchNotifications };