/** * Suspense Query Factory - Generic wrapper for React Suspense-based canister data * * Creates unified fetch/hook/invalidate functions for any canister method. * Works with any Reactor instance. * * Uses `useSuspenseQuery` which: * - Requires wrapping in boundary * - Data is always defined (no undefined checks) * - Does NOT support `enabled` option * * @example * const userQuery = createSuspenseQuery(todoManager, { * functionName: "get_user", * select: (result) => result.user, * }) * * // In component (wrap in Suspense) * const { data: user } = userQuery.useSuspenseQuery() // data is never undefined! */ import type { Reactor, FunctionName, ReactorArgs, TransformKey, } from "@ic-reactor/core" import { useSuspenseQuery } from "@tanstack/react-query" import type { QueryFnData, QueryError, SuspenseQueryConfig, UseSuspenseQueryWithSelect, SuspenseQueryResult, SuspenseQueryFactoryConfig, NoInfer, } from "./types" import { buildChainedSelect } from "./utils" // ============================================================================ // Internal Implementation // ============================================================================ const createSuspenseQueryImpl = < A, M extends FunctionName = FunctionName, T extends TransformKey = "candid", TSelected = QueryFnData, >( reactor: Reactor, config: SuspenseQueryConfig ): SuspenseQueryResult< QueryFnData, 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 = () => reactor.generateQueryKey(params) 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 useSuspenseQueryHook: UseSuspenseQueryWithSelect< TData, TSelected, TError > = (options: any): any => { const baseOptions = reactor.getQueryOptions(params) return useSuspenseQuery( { 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: SuspenseQueryResult< TData, TSelected, TError >["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: SuspenseQueryResult["setData"] = ( updater ) => { return reactor.queryClient.setQueryData(getQueryKey(), updater as any) as | TData | undefined } return { fetch, prefetch, useSuspenseQuery: useSuspenseQueryHook, invalidate, getQueryKey, getCacheData, setData, } } // ============================================================================ // Public Factory Function // ============================================================================ export function createSuspenseQuery< A, T extends TransformKey, M extends FunctionName = FunctionName, TSelected = QueryFnData, >( reactor: Reactor, config: SuspenseQueryConfig, M, T, TSelected> ): SuspenseQueryResult, TSelected, QueryError> { return createSuspenseQueryImpl( reactor, config as SuspenseQueryConfig ) } // ============================================================================ // Convenience: Create suspense query with dynamic args // ============================================================================ export function createSuspenseQueryFactory< A, T extends TransformKey, M extends FunctionName = FunctionName, TSelected = QueryFnData, >( reactor: Reactor, config: SuspenseQueryFactoryConfig, M, T, TSelected> ): ( args: ReactorArgs ) => SuspenseQueryResult, TSelected, QueryError> { const cache = new Map< string, SuspenseQueryResult, 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 = createSuspenseQueryImpl(reactor, { ...(config as SuspenseQueryFactoryConfig), args, }) cache.set(cacheKey, result) return result } }