/** * Query Factory - Generic wrapper for React Query-based canister data * * Creates unified fetch/hook/invalidate functions for any canister method. * Works with any Reactor instance. * * @example * const userQuery = createQuery(todoManager, { * functionName: "get_user", * select: (result) => result.user, * }) * * // In component * const { data: user } = userQuery.useQuery() */ import type { Reactor, FunctionName, ReactorArgs, TransformKey, } from "@ic-reactor/core" import { QueryKey, useQuery } from "@tanstack/react-query" import type { QueryFnData, QueryError, QueryConfig, UseQueryWithSelect, QueryResult, QueryFactoryConfig, NoInfer, } from "./types" import { buildChainedSelect } from "./utils" // ============================================================================ // Internal Implementation // ============================================================================ const createQueryImpl = < A, M extends FunctionName = FunctionName, T extends TransformKey = "candid", TSelected = QueryFnData, >( reactor: Reactor, config: QueryConfig ): QueryResult, TSelected, QueryError> => { type TData = QueryFnData type TError = QueryError const { functionName, args, staleTime = 5 * 60 * 1000, select, queryKey: customQueryKey, ...rest } = config const params = { functionName, args, queryKey: customQueryKey } const getQueryKey = (): QueryKey => reactor.generateQueryKey(params) // Apply config.select to raw data (shared by fetch, getCacheData, and the hook) const applySelect = (raw: TData): TSelected => select ? select(raw) : (raw as unknown as TSelected) /** Cache-first fetch for use in loaders / route preloading. */ const fetch = async (): Promise => { const result = await reactor.fetchQuery(params) return applySelect(result) } /** Fire-and-forget prefetch — warms the cache without blocking. */ const prefetch = (): Promise => { const baseOptions = reactor.getQueryOptions(params) return reactor.queryClient.prefetchQuery({ queryKey: baseOptions.queryKey, queryFn: baseOptions.queryFn, staleTime, }) } const useQueryHook: UseQueryWithSelect = ( options: any ): any => { const baseOptions = reactor.getQueryOptions(params) return useQuery( { queryKey: baseOptions.queryKey, staleTime, ...rest, ...options, queryFn: baseOptions.queryFn, select: buildChainedSelect(select, options?.select), }, reactor.queryClient ) } const invalidate = async (): Promise => { await reactor.queryClient.invalidateQueries({ queryKey: getQueryKey() }) } const getCacheData: QueryResult["getCacheData"] = ( selectFn?: (data: TSelected) => unknown ): any => { const raw = reactor.getQueryData(params) if (raw === undefined) return undefined const selected = applySelect(raw) return selectFn ? selectFn(selected) : selected } const setData: QueryResult["setData"] = ( updater ) => { return reactor.queryClient.setQueryData(getQueryKey(), updater as any) as | TData | undefined } return { fetch, prefetch, useQuery: useQueryHook, invalidate, getQueryKey, getCacheData, setData, } } // ============================================================================ // Public Factory Function // ============================================================================ export function createQuery< A, T extends TransformKey, M extends FunctionName = FunctionName, TSelected = QueryFnData, >( reactor: Reactor, config: QueryConfig, M, T, TSelected> ): QueryResult, TSelected, QueryError> { return createQueryImpl(reactor, config as QueryConfig) } // ============================================================================ // Convenience: Create query with dynamic args // ============================================================================ export function createQueryFactory< A, T extends TransformKey, M extends FunctionName = FunctionName, TSelected = QueryFnData, >( reactor: Reactor, config: QueryFactoryConfig, M, T, TSelected> ): ( args: ReactorArgs ) => QueryResult, TSelected, QueryError> { const cache = new Map< string, QueryResult, TSelected, QueryError> >() return (args: ReactorArgs) => { const key = reactor.generateQueryKey({ functionName: config.functionName as M, args, }) const cacheKey = JSON.stringify(key) const existing = cache.get(cacheKey) if (existing) return existing const result = createQueryImpl(reactor, { ...config, args, }) cache.set(cacheKey, result) return result } }