feat(FontStore): implement state getters, pagination, buildQueryKey, buildOptions

This commit is contained in:
Ilia Mashkov
2026-04-08 09:47:25 +03:00
parent 9a9ff95bf3
commit 778988977f
2 changed files with 47 additions and 14 deletions

View File

@@ -1,7 +1,4 @@
import { import { QueryClient } from '@tanstack/query-core';
type InfiniteData,
QueryClient,
} from '@tanstack/query-core';
import { flushSync } from 'svelte'; import { flushSync } from 'svelte';
import { import {
afterEach, afterEach,

View File

@@ -47,26 +47,49 @@ export class FontStore {
return this.#params; return this.#params;
} }
get fonts(): UnifiedFont[] { get fonts(): UnifiedFont[] {
return []; return this.#result.data?.pages.flatMap((p: FontPage) => p.fonts) ?? [];
} }
get isLoading(): boolean { get isLoading(): boolean {
return false; return this.#result.isLoading;
} }
get isFetching(): boolean { get isFetching(): boolean {
return false; return this.#result.isFetching;
} }
get isError(): boolean { get isError(): boolean {
return false; return this.#result.isError;
} }
get error(): Error | null { get error(): Error | null {
return null; return this.#result.error ?? null;
} }
// isEmpty is false during loading/fetching so the UI never flashes "no results"
// while a fetch is in progress. The !isFetching guard is specifically for the filter-change
// transition: fonts clear synchronously → isFetching becomes true → isEmpty stays false.
get isEmpty(): boolean { get isEmpty(): boolean {
return false; return !this.isLoading && !this.isFetching && this.fonts.length === 0;
} }
get pagination() { get pagination() {
return { total: 0, limit: 50, offset: 0, hasMore: false, page: 1, totalPages: 0 }; const pages = this.#result.data?.pages;
const last = pages?.at(-1);
if (!last) {
return {
total: 0,
limit: this.#params.limit ?? 50,
offset: 0,
hasMore: false,
page: 1,
totalPages: 0,
};
}
return {
total: last.total,
limit: last.limit,
offset: last.offset,
hasMore: this.#result.hasNextPage,
page: pages!.length,
totalPages: Math.ceil(last.total / last.limit),
};
} }
// -- Lifecycle -- // -- Lifecycle --
@@ -124,17 +147,30 @@ export class FontStore {
// -- Private helpers (TypeScript-private so tests can spy via `as any`) -- // -- Private helpers (TypeScript-private so tests can spy via `as any`) --
private buildQueryKey(params: FontStoreParams): readonly unknown[] { private buildQueryKey(params: FontStoreParams): readonly unknown[] {
return ['fonts', params]; const normalized = Object.entries(params).reduce<Record<string, unknown>>((acc, [k, v]) => {
if (v === undefined || v === '' || (Array.isArray(v) && v.length === 0)) return acc;
return { ...acc, [k]: v };
}, {});
return ['fonts', normalized] as const;
} }
private buildOptions(params = this.#params) { private buildOptions(params = this.#params) {
const hasFilters = !!(
params.q
|| (Array.isArray(params.providers) && params.providers.length > 0)
|| (Array.isArray(params.categories) && params.categories.length > 0)
|| (Array.isArray(params.subsets) && params.subsets.length > 0)
);
return { return {
queryKey: this.buildQueryKey(params), queryKey: this.buildQueryKey(params),
queryFn: ({ pageParam }: QueryFunctionContext<readonly unknown[], PageParam>) => queryFn: ({ pageParam }: QueryFunctionContext<readonly unknown[], PageParam>) =>
this.fetchPage({ ...this.#params, ...pageParam }), this.fetchPage({ ...this.#params, ...pageParam }),
initialPageParam: { offset: 0 } as PageParam, initialPageParam: { offset: 0 } as PageParam,
getNextPageParam: (_lastPage: FontPage): PageParam | undefined => undefined, getNextPageParam: (lastPage: FontPage): PageParam | undefined => {
staleTime: 5 * 60 * 1000, const next = lastPage.offset + lastPage.limit;
return next < lastPage.total ? { offset: next } : undefined;
},
staleTime: hasFilters ? 0 : 5 * 60 * 1000,
gcTime: 10 * 60 * 1000, gcTime: 10 * 60 * 1000,
}; };
} }