654 lines
18 KiB
TypeScript
654 lines
18 KiB
TypeScript
import {
|
|
DehydratedState,
|
|
QueryClient,
|
|
CreateInfiniteQueryOptions,
|
|
CreateInfiniteQueryResult,
|
|
CreateMutationOptions,
|
|
CreateMutationResult,
|
|
CreateQueryOptions,
|
|
CreateQueryResult,
|
|
createInfiniteQuery as __useInfiniteQuery,
|
|
createMutation as __useMutation,
|
|
createQuery as __useQuery,
|
|
hashQueryKey,
|
|
useQueryClient,
|
|
QueryClientProviderProps,
|
|
QueryClientProvider,
|
|
} from "@tanstack/solid-query";
|
|
import {
|
|
CreateTRPCClientOptions,
|
|
TRPCClient,
|
|
TRPCClientErrorLike,
|
|
TRPCRequestOptions,
|
|
createTRPCClient,
|
|
} from "@trpc/client";
|
|
import type {
|
|
AnyRouter,
|
|
ProcedureRecord,
|
|
inferHandlerInput,
|
|
inferProcedureClientError,
|
|
inferProcedureInput,
|
|
inferProcedureOutput,
|
|
inferSubscriptionOutput,
|
|
} from "@trpc/server";
|
|
import { inferObservableValue } from "@trpc/server/observable";
|
|
import {
|
|
Accessor,
|
|
Context,
|
|
createEffect,
|
|
createMemo,
|
|
createSignal,
|
|
JSX,
|
|
onCleanup,
|
|
onMount,
|
|
useContext as __useContext,
|
|
} from "solid-js";
|
|
import {
|
|
SSRState,
|
|
TRPCContext,
|
|
TRPCContextProps,
|
|
TRPCContextState,
|
|
} from "../../internals/context";
|
|
import { getArrayQueryKey } from "../../internals/getArrayQueryKey";
|
|
import { CreateTRPCSolidOptions, UseMutationOverride } from "../types";
|
|
|
|
export type OutputWithCursor<TData, TCursor = any> = {
|
|
cursor: TCursor | null;
|
|
data: TData;
|
|
};
|
|
|
|
export interface TRPCReactRequestOptions
|
|
// For RQ, we use their internal AbortSignals instead of letting the user pass their own
|
|
extends Omit<TRPCRequestOptions, "signal"> {
|
|
/**
|
|
* Opt out of SSR for this query by passing `ssr: false`
|
|
*/
|
|
ssr?: boolean;
|
|
/**
|
|
* Opt out or into aborting request on unmount
|
|
*/
|
|
abortOnUnmount?: boolean;
|
|
}
|
|
|
|
export interface TRPCUseQueryBaseOptions {
|
|
/**
|
|
* tRPC-related options
|
|
*/
|
|
trpc?: TRPCReactRequestOptions;
|
|
}
|
|
|
|
export type { TRPCContext, TRPCContextState } from "../../internals/context";
|
|
|
|
export interface UseTRPCQueryOptions<TPath, TInput, TOutput, TData, TError>
|
|
extends CreateQueryOptions<TOutput, TError, TData, () => [TPath, TInput]>,
|
|
TRPCUseQueryBaseOptions {}
|
|
|
|
export interface UseTRPCInfiniteQueryOptions<TPath, TInput, TOutput, TError>
|
|
extends CreateInfiniteQueryOptions<
|
|
TOutput,
|
|
TError,
|
|
TOutput,
|
|
TOutput,
|
|
() => [TPath, TInput]
|
|
>,
|
|
TRPCUseQueryBaseOptions {}
|
|
|
|
export interface UseTRPCMutationOptions<
|
|
TInput,
|
|
TError,
|
|
TOutput,
|
|
TContext = unknown
|
|
> extends CreateMutationOptions<TOutput, TError, TInput, TContext>,
|
|
TRPCUseQueryBaseOptions {}
|
|
|
|
export interface UseTRPCSubscriptionOptions<TOutput, TError> {
|
|
enabled?: boolean;
|
|
onStarted?: () => void;
|
|
onData: (data: TOutput) => void;
|
|
onError?: (err: TError) => void;
|
|
}
|
|
|
|
function getClientArgs<TPathAndInput extends unknown[], TOptions>(
|
|
pathAndInput: TPathAndInput,
|
|
opts: TOptions
|
|
) {
|
|
const [path, input] = pathAndInput;
|
|
return [path, input, (opts as any)?.trpc] as const;
|
|
}
|
|
|
|
type inferInfiniteQueryNames<TObj extends ProcedureRecord> = {
|
|
[TPath in keyof TObj]: inferProcedureInput<TObj[TPath]> extends {
|
|
cursor?: any;
|
|
}
|
|
? TPath
|
|
: never;
|
|
}[keyof TObj];
|
|
|
|
type inferProcedures<TObj extends ProcedureRecord> = {
|
|
[TPath in keyof TObj]: {
|
|
input: inferProcedureInput<TObj[TPath]>;
|
|
output: inferProcedureOutput<TObj[TPath]>;
|
|
};
|
|
};
|
|
|
|
export interface TRPCProviderProps<TRouter extends AnyRouter, TSSRContext>
|
|
extends TRPCContextProps<TRouter, TSSRContext> {
|
|
children: JSX.Element;
|
|
}
|
|
|
|
export type TRPCProvider<TRouter extends AnyRouter, TSSRContext> = (
|
|
props: TRPCProviderProps<TRouter, TSSRContext> & {
|
|
queryClientOpts?: Omit<QueryClientProviderProps, "client">;
|
|
}
|
|
) => JSX.Element;
|
|
|
|
export type UseDehydratedState<TRouter extends AnyRouter> = (
|
|
client: TRPCClient<TRouter>,
|
|
trpcState: DehydratedState | undefined
|
|
) => Accessor<DehydratedState | undefined>;
|
|
|
|
export type CreateClient<TRouter extends AnyRouter> = (
|
|
opts: CreateTRPCClientOptions<TRouter>
|
|
) => TRPCClient<TRouter>;
|
|
|
|
interface TRPCHookResult {
|
|
trpc: {
|
|
path: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
export type UseTRPCQueryResult<TData, TError> = CreateQueryResult<
|
|
TData,
|
|
TError
|
|
> &
|
|
TRPCHookResult;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
export type UseTRPCInfiniteQueryResult<TData, TError> =
|
|
CreateInfiniteQueryResult<TData, TError> & TRPCHookResult;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
export type UseTRPCMutationResult<TData, TError, TVariables, TContext> =
|
|
CreateMutationResult<TData, TError, TVariables, TContext> & TRPCHookResult;
|
|
|
|
/**
|
|
* Makes a stable reference of the `trpc` prop
|
|
*/
|
|
function useHookResult(value: TRPCHookResult["trpc"]): TRPCHookResult["trpc"] {
|
|
const ref = { current: value };
|
|
ref.current.path = value.path;
|
|
return ref.current;
|
|
}
|
|
/**
|
|
* Create strongly typed react hooks
|
|
* @internal
|
|
*/
|
|
export function createHooksInternal<
|
|
TRouter extends AnyRouter,
|
|
TSSRContext = unknown
|
|
>(config?: CreateTRPCSolidOptions<TRouter>) {
|
|
const mutationSuccessOverride: UseMutationOverride["onSuccess"] =
|
|
config?.unstable_overrides?.useMutation?.onSuccess ??
|
|
((options) => options.originalFn());
|
|
|
|
type TQueries = TRouter["_def"]["queries"];
|
|
type TSubscriptions = TRouter["_def"]["subscriptions"];
|
|
type TMutations = TRouter["_def"]["mutations"];
|
|
|
|
type TError = TRPCClientErrorLike<TRouter>;
|
|
type TInfiniteQueryNames = inferInfiniteQueryNames<TQueries>;
|
|
|
|
type TQueryValues = inferProcedures<TQueries>;
|
|
type TMutationValues = inferProcedures<TMutations>;
|
|
|
|
type ProviderContext = Omit<
|
|
TRPCContextState<TRouter, TSSRContext>,
|
|
"ssrState"
|
|
> & {
|
|
ssrState: Accessor<TRPCContextState<TRouter, TSSRContext>["ssrState"]>;
|
|
};
|
|
|
|
const Context = (config?.context ?? TRPCContext) as Context<ProviderContext>;
|
|
const SolidQueryContext = config?.solidQueryContext as Context<
|
|
QueryClient | undefined
|
|
>;
|
|
|
|
const createClient: CreateClient<TRouter> = (opts) => {
|
|
return createTRPCClient(opts);
|
|
};
|
|
|
|
const TRPCProvider: TRPCProvider<TRouter, TSSRContext> = (props) => {
|
|
const { abortOnUnmount = false, client, queryClient, ssrContext } = props;
|
|
const [ssrState, setSSRState] = createSignal<SSRState>(
|
|
props.ssrState ?? false
|
|
);
|
|
onMount(() => {
|
|
// Only updating state to `mounted` if we are using SSR.
|
|
// This makes it so we don't have an unnecessary re-render when opting out of SSR.
|
|
setSSRState((state) => (state ? "mounted" : false));
|
|
});
|
|
return (
|
|
<Context.Provider
|
|
value={{
|
|
abortOnUnmount,
|
|
queryClient,
|
|
client,
|
|
ssrContext: ssrContext || null,
|
|
ssrState,
|
|
fetchQuery: (pathAndInput, opts) => {
|
|
return queryClient.fetchQuery(
|
|
getArrayQueryKey(pathAndInput),
|
|
() => (client as any).query(...getClientArgs(pathAndInput, opts)),
|
|
opts
|
|
);
|
|
},
|
|
fetchInfiniteQuery: (pathAndInput, opts) => {
|
|
return queryClient.fetchInfiniteQuery(
|
|
getArrayQueryKey(pathAndInput),
|
|
({ pageParam }) => {
|
|
const [path, input] = pathAndInput;
|
|
const actualInput = { ...(input as any), cursor: pageParam };
|
|
return (client as any).query(
|
|
...getClientArgs([path, actualInput], opts)
|
|
);
|
|
},
|
|
opts
|
|
);
|
|
},
|
|
|
|
prefetchQuery: (pathAndInput, opts) => {
|
|
return queryClient.prefetchQuery(
|
|
getArrayQueryKey(pathAndInput),
|
|
() => (client as any).query(...getClientArgs(pathAndInput, opts)),
|
|
opts
|
|
);
|
|
},
|
|
prefetchInfiniteQuery: (pathAndInput, opts) => {
|
|
return queryClient.prefetchInfiniteQuery(
|
|
getArrayQueryKey(pathAndInput),
|
|
({ pageParam }) => {
|
|
const [path, input] = pathAndInput;
|
|
const actualInput = { ...(input as any), cursor: pageParam };
|
|
return (client as any).query(
|
|
...getClientArgs([path, actualInput], opts)
|
|
);
|
|
},
|
|
opts
|
|
);
|
|
},
|
|
invalidateQueries: (...args: any[]) => {
|
|
const [queryKey, ...rest] = args;
|
|
return queryClient.invalidateQueries(
|
|
getArrayQueryKey(queryKey),
|
|
...rest
|
|
);
|
|
},
|
|
refetchQueries: (...args: any[]) => {
|
|
const [queryKey, ...rest] = args;
|
|
|
|
return queryClient.refetchQueries(
|
|
getArrayQueryKey(queryKey),
|
|
...rest
|
|
);
|
|
},
|
|
cancelQuery: (pathAndInput) => {
|
|
return queryClient.cancelQueries(getArrayQueryKey(pathAndInput));
|
|
},
|
|
setQueryData: (...args) => {
|
|
const [queryKey, ...rest] = args;
|
|
return queryClient.setQueryData(
|
|
getArrayQueryKey(queryKey),
|
|
...rest
|
|
);
|
|
},
|
|
getQueryData: (...args) => {
|
|
const [queryKey, ...rest] = args;
|
|
|
|
return queryClient.getQueryData(
|
|
getArrayQueryKey(queryKey),
|
|
...rest
|
|
);
|
|
},
|
|
setInfiniteQueryData: (...args) => {
|
|
const [queryKey, ...rest] = args;
|
|
|
|
return queryClient.setQueryData(
|
|
getArrayQueryKey(queryKey),
|
|
...rest
|
|
);
|
|
},
|
|
getInfiniteQueryData: (...args) => {
|
|
const [queryKey, ...rest] = args;
|
|
|
|
return queryClient.getQueryData(
|
|
getArrayQueryKey(queryKey),
|
|
...rest
|
|
);
|
|
},
|
|
}}
|
|
>
|
|
<QueryClientProvider
|
|
client={queryClient}
|
|
{...((props.queryClientOpts ?? {}) as any)}
|
|
>
|
|
{props.children}
|
|
</QueryClientProvider>
|
|
</Context.Provider>
|
|
);
|
|
};
|
|
|
|
function useContext() {
|
|
return __useContext(Context);
|
|
}
|
|
|
|
/**
|
|
* Hack to make sure errors return `status`='error` when doing SSR
|
|
* @link https://github.com/trpc/trpc/pull/1645
|
|
*/
|
|
function useSSRQueryOptionsIfNeeded<
|
|
TOptions extends { retryOnMount?: boolean } | undefined
|
|
>(pathAndInput: unknown[], opts: TOptions): TOptions {
|
|
const { queryClient, ssrState } = useContext();
|
|
return ssrState() &&
|
|
ssrState() !== "mounted" &&
|
|
queryClient.getQueryCache().find(getArrayQueryKey(pathAndInput))?.state
|
|
.status === "error"
|
|
? {
|
|
retryOnMount: false,
|
|
...opts,
|
|
}
|
|
: opts;
|
|
}
|
|
|
|
function useQuery<
|
|
TPath extends keyof TQueryValues & string,
|
|
TQueryFnData = TQueryValues[TPath]["output"],
|
|
TData = TQueryValues[TPath]["output"]
|
|
>(
|
|
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
|
|
opts?: UseTRPCQueryOptions<
|
|
TPath,
|
|
TQueryValues[TPath]["input"],
|
|
TQueryFnData,
|
|
TData,
|
|
TError
|
|
>
|
|
): UseTRPCQueryResult<TData, TError> {
|
|
const ctx = useContext();
|
|
|
|
if (
|
|
typeof window === "undefined" &&
|
|
ctx.ssrState() === "prepass" &&
|
|
opts?.trpc?.ssr !== false &&
|
|
opts?.enabled !== false &&
|
|
!ctx.queryClient.getQueryCache().find(getArrayQueryKey(pathAndInput))
|
|
) {
|
|
void ctx.prefetchQuery(pathAndInput as any, opts as any);
|
|
}
|
|
const ssrOpts = useSSRQueryOptionsIfNeeded(pathAndInput, opts);
|
|
// request option should take priority over global
|
|
const shouldAbortOnUnmount =
|
|
opts?.trpc?.abortOnUnmount ?? ctx?.abortOnUnmount ?? false;
|
|
|
|
const hook = __useQuery(
|
|
() => getArrayQueryKey(pathAndInput),
|
|
(queryFunctionContext) => {
|
|
const actualOpts = {
|
|
...ssrOpts,
|
|
trpc: {
|
|
...ssrOpts?.trpc,
|
|
...(shouldAbortOnUnmount
|
|
? { signal: queryFunctionContext.signal }
|
|
: {}),
|
|
},
|
|
};
|
|
|
|
return (ctx.client as any).query(
|
|
...getClientArgs(pathAndInput, actualOpts)
|
|
);
|
|
},
|
|
{ context: SolidQueryContext, ...ssrOpts } as any
|
|
) as UseTRPCQueryResult<TData, TError>;
|
|
hook.trpc = useHookResult({
|
|
path: pathAndInput[0],
|
|
});
|
|
|
|
return hook;
|
|
}
|
|
|
|
function useMutation<
|
|
TPath extends keyof TMutationValues & string,
|
|
TContext = unknown
|
|
>(
|
|
path: TPath | [TPath],
|
|
opts?: UseTRPCMutationOptions<
|
|
TMutationValues[TPath]["input"],
|
|
TError,
|
|
TMutationValues[TPath]["output"],
|
|
TContext
|
|
>
|
|
): UseTRPCMutationResult<
|
|
TMutationValues[TPath]["output"],
|
|
TError,
|
|
TMutationValues[TPath]["input"],
|
|
TContext
|
|
> {
|
|
const ctx = useContext();
|
|
const queryClient = useQueryClient();
|
|
|
|
const hook = __useMutation(
|
|
(input) => {
|
|
const actualPath = Array.isArray(path) ? path[0] : path;
|
|
|
|
return (ctx.client.mutation as any)(
|
|
...getClientArgs([actualPath, input], opts)
|
|
);
|
|
},
|
|
{
|
|
context: SolidQueryContext,
|
|
...opts,
|
|
onSuccess(...args) {
|
|
const originalFn = () => opts?.onSuccess?.(...args);
|
|
return mutationSuccessOverride({ originalFn, queryClient });
|
|
},
|
|
}
|
|
) as UseTRPCMutationResult<
|
|
TMutationValues[TPath]["output"],
|
|
TError,
|
|
TMutationValues[TPath]["input"],
|
|
TContext
|
|
>;
|
|
|
|
hook.trpc = useHookResult({
|
|
path: Array.isArray(path) ? path[0] : path,
|
|
});
|
|
|
|
return hook;
|
|
}
|
|
|
|
/* istanbul ignore next */
|
|
/**
|
|
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
|
* **Experimental.** API might change without major version bump
|
|
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠
|
|
*/
|
|
function useSubscription<
|
|
TPath extends keyof TSubscriptions & string,
|
|
TOutput extends inferSubscriptionOutput<TRouter, TPath>
|
|
>(
|
|
pathAndInput: [
|
|
path: TPath,
|
|
...args: inferHandlerInput<TSubscriptions[TPath]>
|
|
],
|
|
opts: UseTRPCSubscriptionOptions<
|
|
inferObservableValue<inferProcedureOutput<TSubscriptions[TPath]>>,
|
|
inferProcedureClientError<TSubscriptions[TPath]>
|
|
>
|
|
) {
|
|
const enabled = opts?.enabled ?? true;
|
|
const ctx = useContext();
|
|
|
|
return createEffect(() => {
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
// noop
|
|
(() => {
|
|
return hashQueryKey(pathAndInput);
|
|
})();
|
|
const [path, input] = pathAndInput;
|
|
let isStopped = false;
|
|
const subscription = ctx.client.subscription<
|
|
TRouter["_def"]["subscriptions"],
|
|
TPath,
|
|
TOutput,
|
|
inferProcedureInput<TRouter["_def"]["subscriptions"][TPath]>
|
|
>(path, (input ?? undefined) as any, {
|
|
onStarted: () => {
|
|
if (!isStopped) {
|
|
opts.onStarted?.();
|
|
}
|
|
},
|
|
onData: (data) => {
|
|
if (!isStopped) {
|
|
opts.onData(data);
|
|
}
|
|
},
|
|
onError: (err) => {
|
|
if (!isStopped) {
|
|
opts.onError?.(err);
|
|
}
|
|
},
|
|
});
|
|
onCleanup(() => {
|
|
isStopped = true;
|
|
subscription.unsubscribe();
|
|
});
|
|
});
|
|
}
|
|
|
|
function useInfiniteQuery<TPath extends TInfiniteQueryNames & string>(
|
|
pathAndInput: [
|
|
path: TPath,
|
|
input: Omit<TQueryValues[TPath]["input"], "cursor">
|
|
],
|
|
opts?: UseTRPCInfiniteQueryOptions<
|
|
TPath,
|
|
Omit<TQueryValues[TPath]["input"], "cursor">,
|
|
TQueryValues[TPath]["output"],
|
|
TError
|
|
>
|
|
): UseTRPCInfiniteQueryResult<TQueryValues[TPath]["output"], TError> {
|
|
const [path, input] = pathAndInput;
|
|
const ctx = useContext();
|
|
|
|
if (
|
|
typeof window === "undefined" &&
|
|
ctx.ssrState() === "prepass" &&
|
|
opts?.trpc?.ssr !== false &&
|
|
opts?.enabled !== false &&
|
|
!ctx.queryClient.getQueryCache().find(getArrayQueryKey(pathAndInput))
|
|
) {
|
|
void ctx.prefetchInfiniteQuery(pathAndInput as any, opts as any);
|
|
}
|
|
|
|
const ssrOpts = useSSRQueryOptionsIfNeeded(pathAndInput, opts);
|
|
|
|
// request option should take priority over global
|
|
const shouldAbortOnUnmount =
|
|
opts?.trpc?.abortOnUnmount ?? ctx?.abortOnUnmount ?? false;
|
|
|
|
const hook = __useInfiniteQuery(
|
|
() => getArrayQueryKey(pathAndInput),
|
|
(queryFunctionContext) => {
|
|
const actualOpts = {
|
|
...ssrOpts,
|
|
trpc: {
|
|
...ssrOpts?.trpc,
|
|
...(shouldAbortOnUnmount
|
|
? { signal: queryFunctionContext.signal }
|
|
: {}),
|
|
},
|
|
};
|
|
|
|
const actualInput = {
|
|
...((input as any) ?? {}),
|
|
cursor: queryFunctionContext.pageParam,
|
|
};
|
|
|
|
return (ctx.client as any).query(
|
|
...getClientArgs([path, actualInput], actualOpts)
|
|
);
|
|
},
|
|
{ context: SolidQueryContext, ...ssrOpts } as any
|
|
) as UseTRPCInfiniteQueryResult<TQueryValues[TPath]["output"], TError>;
|
|
|
|
hook.trpc = useHookResult({
|
|
path,
|
|
});
|
|
return hook;
|
|
}
|
|
const useDehydratedState: UseDehydratedState<TRouter> = (
|
|
client,
|
|
trpcState
|
|
) => {
|
|
const transformed: Accessor<DehydratedState | undefined> = createMemo(
|
|
() => {
|
|
if (!trpcState) {
|
|
return trpcState;
|
|
}
|
|
|
|
return client.runtime.transformer.deserialize(trpcState);
|
|
}
|
|
);
|
|
return transformed;
|
|
};
|
|
|
|
return {
|
|
Provider: TRPCProvider,
|
|
createClient,
|
|
useContext,
|
|
useQuery,
|
|
useMutation,
|
|
useSubscription,
|
|
useDehydratedState,
|
|
useInfiniteQuery,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hack to infer the type of `createReactQueryHooks`
|
|
* @link https://stackoverflow.com/a/59072991
|
|
*/
|
|
class GnClass<TRouter extends AnyRouter, TSSRContext = unknown> {
|
|
fn() {
|
|
return createHooksInternal<TRouter, TSSRContext>();
|
|
}
|
|
}
|
|
|
|
type returnTypeInferer<TType> = TType extends (
|
|
a: Record<string, string>
|
|
) => infer U
|
|
? U
|
|
: never;
|
|
type fooType<TRouter extends AnyRouter, TSSRContext = unknown> = GnClass<
|
|
TRouter,
|
|
TSSRContext
|
|
>["fn"];
|
|
|
|
/**
|
|
* Infer the type of a `createSolidQueryHooks` function
|
|
* @internal
|
|
*/
|
|
export type CreateSolidQueryHooks<
|
|
TRouter extends AnyRouter,
|
|
TSSRContext = unknown
|
|
> = returnTypeInferer<fooType<TRouter, TSSRContext>>;
|