import type { QueryClient } from '@tanstack/react-query' import type { FetchContext, ResponseContext, TriggerOptions } from '../../types' interface FetchableDescriptor { key: string fetch?: (ctx: FetchContext) => Promise queryOptions?: { queryKey?: readonly unknown[] staleTime?: number gcTime?: number retry?: number | boolean } } export async function fetchStage( descriptor: FetchableDescriptor, options: TriggerOptions, queryClient: QueryClient, ): Promise { if (!descriptor.fetch) { throw new Error(`No fetch function defined for key "${descriptor.key}"`) } // Include options.params in the queryKey so different param sets are cached // independently by react-query. Native cache is keyed by descriptor.key only // (managed at the engine level, not here). const baseQueryKey = descriptor.queryOptions?.queryKey ?? [descriptor.key] const queryKey = options.params && Object.keys(options.params).length > 0 ? [...baseQueryKey, options.params] : baseQueryKey const staleTime = options.force ? 0 : (descriptor.queryOptions?.staleTime ?? Infinity) if (options.force) { await queryClient.invalidateQueries({ queryKey }) } const meta: Record = {} const fetchCtx: FetchContext = { key: descriptor.key, options, queryClient, meta, } const start = Date.now() const response = await queryClient.fetchQuery({ queryKey, queryFn: async () => { const data = await descriptor.fetch?.(fetchCtx) meta.source = 'api' return data }, staleTime, gcTime: descriptor.queryOptions?.gcTime, retry: descriptor.queryOptions?.retry ?? false, }) return { key: descriptor.key, response, duration: Date.now() - start, source: (meta.source as ResponseContext['source']) ?? 'query-cache', meta, } }