Merge branch 'copilot/investigate-performance-regression' into develop
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/node_modules/
|
||||
yarn-error.log*
|
||||
coverage/
|
||||
coverage/
|
||||
package-lock.json
|
||||
|
||||
@ -72,8 +72,9 @@ interface IAntennaAccountsForm {
|
||||
const AntennaAccountsForm: React.FC<IAntennaAccountsForm> = ({ antennaId, excluded = false }) => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { data: accountIds = [], isFetching: isFetchingAccounts } = useAntennaAccounts(antennaId);
|
||||
const { data: excludedAccountIds = [], isFetching: isFetchingExcludedAccounts } =
|
||||
const { data: accountIds = [] as Array<string>, isFetching: isFetchingAccounts } =
|
||||
useAntennaAccounts(antennaId);
|
||||
const { data: excludedAccountIds = [] as Array<string>, isFetching: isFetchingExcludedAccounts } =
|
||||
useAntennaExcludedAccounts(antennaId);
|
||||
const { data: searchAccountIds = [] } = useAccountSearch(searchValue, {
|
||||
following: true,
|
||||
|
||||
@ -45,7 +45,8 @@ const CircleEditorModal: React.FC<BaseModalProps & CircleEditorModalProps> = ({
|
||||
|
||||
const { data: circle } = useCircle(circleId);
|
||||
const { mutate: updateCircle, isPending: disabled } = useUpdateCircle(circleId);
|
||||
const { data: accountIds = [], isFetching: isFetchingAccounts } = useCircleAccounts(circleId);
|
||||
const { data: accountIds = [] as Array<string>, isFetching: isFetchingAccounts } =
|
||||
useCircleAccounts(circleId);
|
||||
const { data: searchAccountIds = [] } = useAccountSearch(searchValue, {
|
||||
following: true,
|
||||
limit: 5,
|
||||
|
||||
@ -22,7 +22,7 @@ interface IListMembersForm {
|
||||
const ListMembersForm: React.FC<IListMembersForm> = ({ listId }) => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { data: accountIds = [], isFetching } = useListAccounts(listId);
|
||||
const { data: accountIds = [] as Array<string>, isFetching } = useListAccounts(listId);
|
||||
const { data: searchAccountIds = [] } = useAccountSearch(searchValue, {
|
||||
following: true,
|
||||
limit: 5,
|
||||
|
||||
@ -48,14 +48,11 @@ const makePaginatedResponseQueryOptions =
|
||||
}
|
||||
|
||||
if (Array.isArray(lastPage.items)) {
|
||||
const items = new PaginatedResponseArray(
|
||||
...data.pages.flatMap((page) =>
|
||||
const items = PaginatedResponseArray.from(
|
||||
data.pages.flatMap((page) =>
|
||||
Array.isArray(page.items) ? page.items : [page.items],
|
||||
),
|
||||
);
|
||||
|
||||
items.total = lastPage.total;
|
||||
items.partial = lastPage.partial;
|
||||
).setMeta(lastPage.total, lastPage.partial);
|
||||
|
||||
return items as T3;
|
||||
}
|
||||
|
||||
@ -11,8 +11,23 @@ import { useOwnAccount } from '@/hooks/use-own-account';
|
||||
import type { PaginatedResponse, PlApiClient } from 'pl-api';
|
||||
|
||||
class PaginatedResponseArray<T> extends Array<T> {
|
||||
total?: number;
|
||||
partial?: boolean;
|
||||
declare total: number | undefined;
|
||||
declare partial: boolean | undefined;
|
||||
|
||||
static override from<T>(items: ArrayLike<T> | Iterable<T>): PaginatedResponseArray<T> {
|
||||
const arr = new PaginatedResponseArray<T>();
|
||||
for (const item of Array.from(items)) {
|
||||
arr.push(item);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/** 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 });
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
type PaginatedResponseQueryResult<T, IsArray extends boolean> = IsArray extends true
|
||||
@ -59,14 +74,11 @@ const makePaginatedResponseQuery =
|
||||
}
|
||||
|
||||
if (Array.isArray(lastPage.items)) {
|
||||
const items = new PaginatedResponseArray(
|
||||
...data.pages.flatMap((page) =>
|
||||
const items = PaginatedResponseArray.from(
|
||||
data.pages.flatMap((page) =>
|
||||
Array.isArray(page.items) ? page.items : [page.items],
|
||||
),
|
||||
);
|
||||
|
||||
items.total = lastPage.total;
|
||||
items.partial = lastPage.partial;
|
||||
).setMeta(lastPage.total, lastPage.partial);
|
||||
|
||||
return items as T3;
|
||||
}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { PaginatedResponseArray } from '@/queries/utils/make-paginated-response-query';
|
||||
|
||||
/**
|
||||
* TanStack Query's isPlainArray check used for structural sharing.
|
||||
* If this returns false, structural sharing breaks, causing new references
|
||||
* on every render and leading to performance degradation.
|
||||
*/
|
||||
const isPlainArray = (value: unknown): boolean =>
|
||||
Array.isArray(value) && (value as Array<unknown>).length === Object.keys(value as object).length;
|
||||
|
||||
describe('PaginatedResponseArray', () => {
|
||||
it('creates an array from items without spread operator', () => {
|
||||
const items = PaginatedResponseArray.from([1, 2, 3]);
|
||||
expect(items).toEqual([1, 2, 3]);
|
||||
expect(items.length).toBe(3);
|
||||
expect(items instanceof PaginatedResponseArray).toBe(true);
|
||||
});
|
||||
|
||||
it('handles large arrays without stack overflow', () => {
|
||||
const largeArray = Array.from({ length: 100_000 }, (_, i) => i);
|
||||
const items = PaginatedResponseArray.from(largeArray);
|
||||
expect(items.length).toBe(100_000);
|
||||
});
|
||||
|
||||
it('supports non-enumerable total and partial properties via setMeta', () => {
|
||||
const items = PaginatedResponseArray.from(['a', 'b', 'c']).setMeta(42, true);
|
||||
|
||||
expect(items.total).toBe(42);
|
||||
expect(items.partial).toBe(true);
|
||||
});
|
||||
|
||||
it('passes isPlainArray check with non-enumerable properties for structural sharing', () => {
|
||||
const items = PaginatedResponseArray.from([1, 2, 3]).setMeta(10, false);
|
||||
|
||||
expect(isPlainArray(items)).toBe(true);
|
||||
});
|
||||
|
||||
it('total and partial properties are writable after setMeta', () => {
|
||||
const items = PaginatedResponseArray.from([1]).setMeta(5, false);
|
||||
|
||||
items.total = 10;
|
||||
expect(items.total).toBe(10);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user