fix: Fix undefined query data and add fallback to Fontshare API

- Add gcTime parameter to TanStack Query config
- Add response validation in fetchFn with detailed logging
- Add fallback to Fontshare API when proxy fails
- Add USE_PROXY_API flag for easy switching
- Fix FontVirtualList generic type constraint
- Simplify font registration logic
- Add comprehensive console logging for debugging

Fixes: Query data cannot be undefined error
This commit is contained in:
Ilia Mashkov
2026-01-29 15:20:51 +03:00
parent dc72b9e048
commit 471e186e70
5 changed files with 525 additions and 20 deletions

View File

@@ -7,6 +7,8 @@
* Proxy API normalizes font data from Google Fonts and Fontshare into a single
* unified format, eliminating the need for client-side normalization.
*
* Fallback: If proxy API fails, falls back to Fontshare API for development.
*
* @see https://api.glyphdiff.com/api/v1/fonts
*/
@@ -24,6 +26,19 @@ import type {
*/
const PROXY_API_URL = 'https://api.glyphdiff.com/api/v1/fonts' as const;
/**
* Whether to use proxy API (true) or fallback (false)
*
* Set to true when your proxy API is ready:
* const USE_PROXY_API = true;
*
* Set to false to use Fontshare API as fallback during development:
* const USE_PROXY_API = false;
*
* The app will automatically fall back to Fontshare API if the proxy fails.
*/
const USE_PROXY_API = true;
/**
* Proxy API parameters
*
@@ -93,6 +108,8 @@ export interface ProxyFontsResponse {
/**
* Fetch fonts from proxy API
*
* If proxy API fails or is unavailable, falls back to Fontshare API for development.
*
* @param params - Query parameters for filtering and pagination
* @returns Promise resolving to proxy API response
* @throws ApiError when request fails
@@ -121,19 +138,84 @@ export interface ProxyFontsResponse {
export async function fetchProxyFonts(
params: ProxyFontsParams = {},
): Promise<ProxyFontsResponse> {
const queryString = buildQueryString(params);
const url = `${PROXY_API_URL}${queryString}`;
// Try proxy API first if enabled
if (USE_PROXY_API) {
try {
const queryString = buildQueryString(params);
const url = `${PROXY_API_URL}${queryString}`;
try {
const response = await api.get<ProxyFontsResponse>(url);
return response.data;
} catch (error) {
// Re-throw ApiError with context
if (error instanceof Error) {
throw error;
console.log('[fetchProxyFonts] Fetching from proxy API', { params, url });
const response = await api.get<ProxyFontsResponse>(url);
// Validate response has fonts array
if (!response.data || !Array.isArray(response.data.fonts)) {
console.error('[fetchProxyFonts] Invalid response from proxy API', response.data);
throw new Error('Proxy API returned invalid response');
}
console.log('[fetchProxyFonts] Proxy API success', {
count: response.data.fonts.length,
});
return response.data;
} catch (error) {
console.warn('[fetchProxyFonts] Proxy API failed, using fallback', error);
// Check if it's a network error or proxy not available
const isNetworkError = error instanceof Error
&& (error.message.includes('Failed to fetch')
|| error.message.includes('Network')
|| error.message.includes('404')
|| error.message.includes('500'));
if (isNetworkError) {
// Fall back to Fontshare API
console.log('[fetchProxyFonts] Using Fontshare API as fallback');
return await fetchFontshareFallback(params);
}
// Re-throw other errors
if (error instanceof Error) {
throw error;
}
throw new Error(`Failed to fetch fonts from proxy API: ${String(error)}`);
}
throw new Error(`Failed to fetch fonts from proxy API: ${String(error)}`);
}
// Use Fontshare API directly
console.log('[fetchProxyFonts] Using Fontshare API (proxy disabled)');
return await fetchFontshareFallback(params);
}
/**
* Fallback to Fontshare API when proxy is unavailable
*
* Maps proxy API params to Fontshare API params and normalizes response
*/
async function fetchFontshareFallback(
params: ProxyFontsParams,
): Promise<ProxyFontsResponse> {
// Import dynamically to avoid circular dependency
const { fetchFontshareFonts } = await import('../fontshare/fontshare');
const { normalizeFontshareFonts } = await import('../../lib/normalize/normalize');
// Map proxy params to Fontshare params
const fontshareParams = {
q: params.q,
categories: params.category ? [params.category] : undefined,
page: params.offset ? Math.floor(params.offset / (params.limit || 50)) + 1 : undefined,
limit: params.limit,
};
const response = await fetchFontshareFonts(fontshareParams);
const normalizedFonts = normalizeFontshareFonts(response.fonts);
return {
fonts: normalizedFonts,
total: response.count_total,
limit: params.limit || response.count,
offset: params.offset || 0,
};
}
/**
@@ -156,5 +238,11 @@ export async function fetchProxyFontById(
id: string,
): Promise<UnifiedFont | undefined> {
const response = await fetchProxyFonts({ limit: 1000, q: id });
if (!response || !response.fonts) {
console.error('[fetchProxyFontById] No fonts in response', { response });
return undefined;
}
return response.fonts.find(font => font.id === id);
}

View File

@@ -59,6 +59,7 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
queryKey: this.getQueryKey(params),
queryFn: () => this.fetchFn(params),
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
};
}

View File

@@ -111,11 +111,29 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
protected async fetchFn(params: ProxyFontsParams): Promise<UnifiedFont[]> {
const response = await fetchProxyFonts(params);
// Validate response structure
if (!response) {
console.error('[UnifiedFontStore] fetchProxyFonts returned undefined', { params });
throw new Error('Proxy API returned undefined response');
}
if (!response.fonts) {
console.error('[UnifiedFontStore] response.fonts is undefined', { response });
throw new Error('Proxy API response missing fonts array');
}
if (!Array.isArray(response.fonts)) {
console.error('[UnifiedFontStore] response.fonts is not an array', {
fonts: response.fonts,
});
throw new Error('Proxy API fonts is not an array');
}
// Store pagination metadata separately for derived values
this.#paginationMetadata = {
total: response.total,
limit: response.limit,
offset: response.offset,
total: response.total ?? 0,
limit: response.limit ?? this.params.limit ?? 50,
offset: response.offset ?? this.params.offset ?? 0,
};
return response.fonts;

View File

@@ -3,7 +3,7 @@
- Renders a virtualized list of fonts
- Handles font registration with the manager
-->
<script lang="ts" generics="T extends ({ id: string } | [{ id: string }, { id: string }])">
<script lang="ts" generics="T extends { id: string }">
import { VirtualList } from '$shared/ui';
import type { ComponentProps } from 'svelte';
import { appliedFontsManager } from '../../model';
@@ -16,12 +16,7 @@ let { items, children, onVisibleItemsChange, ...rest }: Props = $props();
function handleInternalVisibleChange(visibleItems: T[]) {
// Auto-register fonts with the manager
const slugs = visibleItems.map(item => {
if (Array.isArray(item)) {
return item.map(font => font.id);
}
return item.id;
}).flat();
const slugs = visibleItems.map(item => item.id);
appliedFontsManager.registerFonts(slugs);
// Forward the call to any external listener