diff --git a/examples/.experimental/next-app-dir/src/app/standalone/_lib/cacheLink.ts b/examples/.experimental/next-app-dir/src/app/standalone/_lib/cacheLink.ts new file mode 100644 index 00000000000..f698af85571 --- /dev/null +++ b/examples/.experimental/next-app-dir/src/app/standalone/_lib/cacheLink.ts @@ -0,0 +1,195 @@ +import type { + TRPCClientError, + TRPCLink, + TRPCLinkDecoratorObject, +} from '@trpc/client'; +import type { AnyTRPCRouter } from '@trpc/server'; +import { observable, share, tap } from '@trpc/server/observable'; +/* istanbul ignore file -- @preserve */ +// We're not actually exporting this link +import type { Observable, Unsubscribable } from '@trpc/server/observable'; +import type { + AnyRouter, + InferrableClientTypes, + ProcedureType, +} from '@trpc/server/unstable-core-do-not-import'; + +export const normalize = (opts: { + path: string[] | string; + input: unknown; + type: ProcedureType; +}) => { + return JSON.stringify({ + path: Array.isArray(opts.path) ? opts.path.join('.') : opts.path, + input: opts.input, + type: opts.type, + }); +}; + +/** + * @link https://trpc.io/docs/v11/client/links/cacheLink + */ +export function cacheLink(): TRPCLink< + TRoot, + TRPCLinkDecoratorObject<{ + query: { + /** + * If true, the cache will be ignored and the request will be made as if it was the first time + */ + ignoreCache: boolean; + }; + runtime: { + cache: Record< + string, + { + observable: Observable>; + } + >; + }; + }> +> { + // initialized config + return (runtime) => { + // initialized in app + const cache: Record< + string, + { + observable: Observable>; + } + > = {}; + runtime.cache = cache; + return (opts) => { + const { op } = opts; + if (op.type !== 'query') { + return opts.next(opts.op); + } + const normalized = normalize({ + input: opts.op.input, + path: opts.op.path, + type: opts.op.type, + }); + + op.ignoreCache; + // ^? + + let cached = cache[normalized]; + if (!cached) { + console.log('found cache entry'); + cached = cache[normalized] = { + observable: observable((observer) => { + const subscription = opts.next(opts.op).subscribe({ + next(v) { + console.log(`got new value for ${normalized} in cacheLink`); + observer.next(v); + }, + error(e) { + observer.error(e); + }, + complete() { + observer.complete(); + }, + }); + return () => { + subscription.unsubscribe(); + }; + }).pipe(share()), + }; + } + + console.log({ cached }); + + return cached.observable; + }; + }; +} + +/** + * @link https://trpc.io/docs/v11/client/links/loggerLink + */ +export function testDecorationLink( + // eslint-disable-next-line @typescript-eslint/ban-types + _opts: {} = {}, +): TRPCLink< + TRoot, + TRPCLinkDecoratorObject<{ + query: { + /** + * I'm just here for testing inference + */ + __fromTestLink1: true; + }; + mutation: { + /** + * I'm just here for testing inference + */ + __fromTestLink2: true; + }; + }> +> { + return () => { + return (opts) => { + return observable((observer) => { + return opts + .next(opts.op) + .pipe( + tap({ + next(result) { + // logResult(result); + }, + error(result) { + // logResult(result); + }, + }), + ) + .subscribe(observer); + }); + }; + }; +} + +export function refetchLink(): TRPCLink { + return () => { + return ({ op, next }) => { + if (typeof document === 'undefined') { + return next(op); + } + return observable((observer) => { + console.log('------------------ fetching refetchLink'); + let next$: Unsubscribable | null = null; + let nextTimer: ReturnType | null = null; + let attempts = 0; + let isDone = false; + function attempt() { + console.log('fetching.......'); + attempts++; + next$?.unsubscribe(); + next$ = next(op).subscribe({ + error(error) { + observer.error(error); + }, + next(result) { + observer.next(result); + + if (nextTimer) { + clearTimeout(nextTimer); + } + nextTimer = setTimeout(() => { + attempt(); + }, 3000); + }, + complete() { + if (isDone) { + observer.complete(); + } + }, + }); + } + attempt(); + return () => { + isDone = true; + next$?.unsubscribe(); + }; + }); + }; + }; +} diff --git a/examples/.experimental/next-app-dir/src/app/standalone/_lib/createReactClient.tsx b/examples/.experimental/next-app-dir/src/app/standalone/_lib/createReactClient.tsx new file mode 100644 index 00000000000..34e8698ae20 --- /dev/null +++ b/examples/.experimental/next-app-dir/src/app/standalone/_lib/createReactClient.tsx @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { + CreateTRPCClient, + TRPCDecoratedClientOptions, + TRPCLinkDecoration, +} from '@trpc/client'; +import { createTRPCClient, getUntypedClient } from '@trpc/client'; +import type { + AnyTRPCRouter, + ProcedureType, + TRPCProcedureType, +} from '@trpc/server'; +import type { Unsubscribable } from '@trpc/server/observable'; +import { observableToPromise } from '@trpc/server/observable'; +import { createRecursiveProxy } from '@trpc/server/unstable-core-do-not-import'; +import React, { use, useContext, useEffect, useRef } from 'react'; + +function getBaseUrl() { + if (typeof window !== 'undefined') return ''; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return 'http://localhost:3000'; +} + +function getUrl() { + return getBaseUrl() + '/api/trpc'; +} + +const normalize = (opts: { + path: string[]; + input: unknown; + type: ProcedureType; +}) => { + return JSON.stringify(opts); +}; +export function createReactClient< + TRouter extends AnyTRPCRouter, + TDecoration extends TRPCLinkDecoration, +>(getOptions: () => TRPCDecoratedClientOptions) { + type $Client = CreateTRPCClient; + type Context = { + client: $Client; + }; + const Provider = React.createContext(null as unknown as Context); + return { + /** + * @deprecated temporary hack to debug types + */ + $types: {} as unknown as { + decoration: TDecoration; + }, + Provider: (props: { children: React.ReactNode }) => { + const [client] = React.useState(() => { + const options = getOptions(); + return createTRPCClient(options); + }); + + return ( + + {props.children} + + ); + }, + useClient: () => { + const ctx = useContext(Provider); + + // force rendered + const [renderCount, setRenderCount] = React.useState(0); + const forceRender = React.useCallback(() => { + setRenderCount((c) => c + 1); + }, []); + + type Track = { + promise: Promise; + unsub: Unsubscribable; + }; + const trackRef = useRef(new Map()); + console.log('--------------', trackRef.current); + useEffect(() => { + const tracked = trackRef.current; + return () => { + console.log('unsubscribing'); + + tracked.forEach((val) => { + val.unsub.unsubscribe(); + }); + }; + }, []); + useEffect(() => { + console.log(`rendered ${renderCount}`); + }, [renderCount]); + if (!ctx) { + throw new Error('No tRPC client found'); + } + const untyped = getUntypedClient(ctx.client); + + return createRecursiveProxy((opts) => { + console.log('opts', opts); + const path = [...opts.path]; + const type = path.pop()! as TRPCProcedureType; + if (type !== 'query') { + throw new Error('only queries are supported'); + } + + const input = opts.args[0]; + + const normalized = normalize({ path, input, type }); + + let tracked = trackRef.current.get(normalized); + if (!tracked) { + console.log('tracking new query', normalized, trackRef.current); + + tracked = {} as Track; + const observable = untyped.$request({ + type, + input, + path: path.join('.'), + }); + + tracked.promise = new Promise((resolve, reject) => { + let first = true; + const unsub = observable.subscribe({ + next(val) { + console.log('got new value in useClient'); + if (first) { + resolve(val.result.data); + first = false; + } else { + console.log('cache update'); + // something made the observable emit again, probably a cache update + + // reset promise + tracked!.promise = Promise.resolve(val.result.data); + + // force re-render + forceRender(); + } + }, + error() { + // [...?] + }, + }); + tracked!.unsub = unsub; + }); + + console.log('saving', normalized); + trackRef.current.set(normalized, tracked); + } + + return tracked.promise; + }) as $Client; + }, + }; +} diff --git a/examples/.experimental/next-app-dir/src/app/standalone/_provider.tsx b/examples/.experimental/next-app-dir/src/app/standalone/_provider.tsx new file mode 100644 index 00000000000..ac85198aa12 --- /dev/null +++ b/examples/.experimental/next-app-dir/src/app/standalone/_provider.tsx @@ -0,0 +1,53 @@ +'use client'; + +import type { inferTRPCClientOptionTypes } from '@trpc/client'; +import { + createTRPCClientOptions, + httpBatchLink, + loggerLink, +} from '@trpc/client'; +import type { AppRouter } from '~/server/routers/_app'; +import { getUrl } from '~/trpc/shared'; +import superjson from 'superjson'; +import { cacheLink, refetchLink, testDecorationLink } from './_lib/cacheLink'; +import { createReactClient } from './_lib/createReactClient'; + +const getTrpcOptions = createTRPCClientOptions()(() => ({ + links: [ + cacheLink(), + testDecorationLink(), + refetchLink(), + loggerLink({ + enabled: (op) => true, + }), + httpBatchLink({ + transformer: superjson, + url: getUrl(), + headers() { + return { + 'x-trpc-source': 'standalone', + }; + }, + }), + ], +})); + +type $Decoration = inferTRPCClientOptionTypes; +// ^? +type T = $Decoration['query']['ignoreCache']; +type T4 = $Decoration['query']['__fromTestLink1']; + +type T3 = $Decoration['_debug']['$Declarations']; + +// type IsEqual = T extends U ? (U extends T1 ? true : false) : false; + +type T2 = $Decoration['_debug']['$Links'][1]; +// ^? + +export const standaloneClient = createReactClient(getTrpcOptions); +standaloneClient.$types.decoration; +export function Provider(props: { children: React.ReactNode }) { + return ( + {props.children} + ); +} diff --git a/examples/.experimental/next-app-dir/src/app/standalone/layout.tsx b/examples/.experimental/next-app-dir/src/app/standalone/layout.tsx new file mode 100644 index 00000000000..f0c3676806b --- /dev/null +++ b/examples/.experimental/next-app-dir/src/app/standalone/layout.tsx @@ -0,0 +1,5 @@ +import { Provider } from './_provider'; + +export default function Layout(props: { children: React.ReactNode }) { + return {props.children}; +} diff --git a/examples/.experimental/next-app-dir/src/app/standalone/page.tsx b/examples/.experimental/next-app-dir/src/app/standalone/page.tsx new file mode 100644 index 00000000000..dfc59bd44c4 --- /dev/null +++ b/examples/.experimental/next-app-dir/src/app/standalone/page.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { Suspense, use, useEffect, useState } from 'react'; +import { standaloneClient } from './_provider'; + +export default function Page() { + const client = standaloneClient.useClient(); + const [, forceRender] = useState(0); + + // useEffect(() => { + // // force render every second + // const interval = setInterval(() => { + // console.log('force rendering the page'); + // forceRender((c) => c + 1); + // }, 1000); + + // return () => { + // clearInterval(interval); + // }; + // }, []); + + const promise = client.greeting.query( + { + text: 'standalone client', + }, + { + ignoreCache: true, + // wohooo, type and typedoc is inferred from cacheLink + }, + ); + return ( + <> + +
{JSON.stringify(use(promise), null, 4)}
+
+ + ); +} diff --git a/examples/.experimental/next-app-dir/src/server/routers/_app.ts b/examples/.experimental/next-app-dir/src/server/routers/_app.ts index 50891e0abda..bcefc0fbadb 100644 --- a/examples/.experimental/next-app-dir/src/server/routers/_app.ts +++ b/examples/.experimental/next-app-dir/src/server/routers/_app.ts @@ -34,7 +34,9 @@ export const appRouter = router({ ) .query(async (opts) => { console.log('request from', opts.ctx.headers?.['x-trpc-source']); - return `hello ${opts.input.text} - ${Math.random()}`; + return `hello ${opts.input.text} - current second: ${Math.round( + new Date().getTime() / 1000, + )}`; }), secret: publicProcedure.query(async (opts) => { diff --git a/examples/.experimental/next-app-dir/src/server/trpc.ts b/examples/.experimental/next-app-dir/src/server/trpc.ts index 19ba090e443..6efd05cc268 100644 --- a/examples/.experimental/next-app-dir/src/server/trpc.ts +++ b/examples/.experimental/next-app-dir/src/server/trpc.ts @@ -8,19 +8,19 @@ import type { Context } from './context'; const t = initTRPC.context().create({ transformer: superjson, - errorFormatter(opts) { - const { shape, error } = opts; - return { - ...shape, - data: { - ...shape.data, - zodError: - error.code === 'BAD_REQUEST' && error.cause instanceof ZodError - ? error.cause.flatten() - : null, - }, - }; - }, + // errorFormatter(opts) { + // const { shape, error } = opts; + // return { + // ...shape, + // data: { + // ...shape.data, + // zodError: + // error.code === 'BAD_REQUEST' && error.cause instanceof ZodError + // ? error.cause.flatten() + // : null, + // }, + // }; + // }, }); export const router = t.router; diff --git a/packages/client/src/createTRPCClient.ts b/packages/client/src/createTRPCClient.ts index 431c6ddcf21..83efc3b07b0 100644 --- a/packages/client/src/createTRPCClient.ts +++ b/packages/client/src/createTRPCClient.ts @@ -7,7 +7,6 @@ import type { inferProcedureInput, inferTransformedProcedureOutput, IntersectionError, - ProcedureOptions, ProcedureType, RouterRecord, } from '@trpc/server/unstable-core-do-not-import'; @@ -15,19 +14,24 @@ import { createFlatProxy, createRecursiveProxy, } from '@trpc/server/unstable-core-do-not-import'; +import type { TRPCDecoratedClientOptions } from './createTRPCClientOptions'; import type { CreateTRPCClientOptions } from './createTRPCUntypedClient'; import type { TRPCSubscriptionObserver, UntypedClientProperties, } from './internals/TRPCUntypedClient'; import { TRPCUntypedClient } from './internals/TRPCUntypedClient'; +import type { TRPCLinkDecoration } from './links'; +import type { TRPCRequestOptions } from './links/types'; import type { TRPCClientError } from './TRPCClientError'; /** * @public **/ -export type inferRouterClient = - DecoratedProcedureRecord; +export type inferRouterClient< + TRouter extends AnyRouter, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, +> = DecoratedProcedureRecord; type ResolverDef = { input: any; @@ -37,33 +41,41 @@ type ResolverDef = { }; /** @internal */ -export type Resolver = ( +export type ClientProcedureCall< + TDef extends ResolverDef, + TType extends ProcedureType, + TDecoration extends TRPCLinkDecoration, +> = ( input: TDef['input'], - opts?: ProcedureOptions, + opts?: Partial>, ) => Promise; -type SubscriptionResolver = ( +type SubscriptionResolver< + TDef extends ResolverDef, + TDecoration extends TRPCLinkDecoration, +> = ( input: TDef['input'], opts?: Partial< - TRPCSubscriptionObserver> - > & - ProcedureOptions, + TRPCSubscriptionObserver> & + TDecoration['subscription'] + >, ) => Unsubscribable; type DecorateProcedure< TType extends ProcedureType, TDef extends ResolverDef, + TDecoration extends TRPCLinkDecoration, > = TType extends 'query' ? { - query: Resolver; + query: ClientProcedureCall; } : TType extends 'mutation' ? { - mutate: Resolver; + mutate: ClientProcedureCall; } : TType extends 'subscription' ? { - subscribe: SubscriptionResolver; + subscribe: SubscriptionResolver; } : never; @@ -73,10 +85,11 @@ type DecorateProcedure< type DecoratedProcedureRecord< TRouter extends AnyRouter, TRecord extends RouterRecord, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, > = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends RouterRecord - ? DecoratedProcedureRecord + ? DecoratedProcedureRecord : $Value extends AnyProcedure ? DecorateProcedure< $Value['_def']['type'], @@ -88,14 +101,15 @@ type DecoratedProcedureRecord< >; errorShape: inferClientTypes['errorShape']; transformer: inferClientTypes['transformer']; - } + }, + TDecoration > : never : never; }; const clientCallTypeMap: Record< - keyof DecorateProcedure, + keyof DecorateProcedure, ProcedureType > = { query: 'query', @@ -113,12 +127,14 @@ export const clientCallTypeToProcedureType = ( /** * Creates a proxy client and shows type errors if you have query names that collide with built-in properties */ -export type CreateTRPCClient = - inferRouterClient extends infer $Value - ? UntypedClientProperties & keyof $Value extends never - ? inferRouterClient - : IntersectionError - : never; +export type CreateTRPCClient< + TRouter extends AnyRouter, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, +> = inferRouterClient extends infer $Value + ? UntypedClientProperties & keyof $Value extends never + ? inferRouterClient + : IntersectionError + : never; /** * @internal @@ -144,10 +160,16 @@ export function createTRPCClientProxy( }); } -export function createTRPCClient( - opts: CreateTRPCClientOptions, +type FIXME = any; +export function createTRPCClient< + TRouter extends AnyRouter, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, +>( + opts: + | CreateTRPCClientOptions + | TRPCDecoratedClientOptions, ): CreateTRPCClient { - const client = new TRPCUntypedClient(opts); + const client = new TRPCUntypedClient(opts as FIXME); const proxy = createTRPCClientProxy(client); return proxy; } diff --git a/packages/client/src/createTRPCClientOptions.ts b/packages/client/src/createTRPCClientOptions.ts new file mode 100644 index 00000000000..382cd215a2c --- /dev/null +++ b/packages/client/src/createTRPCClientOptions.ts @@ -0,0 +1,63 @@ +import type { + InferrableClientTypes, + Simplify, + Unwrap, +} from '@trpc/server/unstable-core-do-not-import'; +import type { TRPCLink, TRPCLinkDecoration } from './links'; + +const typesSymbol = Symbol('createTRPCClientOptions'); + +type UnionToIntersection = ( + TUnion extends any ? (x: TUnion) => void : never +) extends (x: infer I) => void + ? I + : never; + +export function createTRPCClientOptions() { + return <$Links extends TRPCLink[]>( + callback: () => { + links: [...$Links]; + }, + ) => { + type $Declarations = { + [TKey in keyof $Links]: $Links[TKey] extends TRPCLink< + any, + infer TDeclaration + > + ? TRPCLinkDecoration extends TDeclaration + ? never + : TDeclaration + : never; + }[number]; + + type $Merged = { + [TKey in keyof TRPCLinkDecoration]: TKey extends keyof $Declarations + ? Simplify> + : // eslint-disable-next-line @typescript-eslint/ban-types + {}; + }; + return callback as unknown as () => TRPCDecoratedClientOptions< + TRoot, + $Merged & { + _debug: { + $Declarations: $Declarations; + $Links: $Links; + }; + } + >; + }; +} + +export type TRPCDecoratedClientOptions< + TRoot extends InferrableClientTypes, + TDecoration extends TRPCLinkDecoration, +> = { + links: TRPCLink[]; + [typesSymbol]: TDecoration; +}; + +export type inferTRPCClientOptionTypes< + TOptions extends + | TRPCDecoratedClientOptions + | (() => TRPCDecoratedClientOptions), +> = Unwrap[typeof typesSymbol]; diff --git a/packages/client/src/createTRPCUntypedClient.ts b/packages/client/src/createTRPCUntypedClient.ts index d9aa68d3548..091445c9f3f 100644 --- a/packages/client/src/createTRPCUntypedClient.ts +++ b/packages/client/src/createTRPCUntypedClient.ts @@ -8,8 +8,5 @@ export function createTRPCUntypedClient( return new TRPCUntypedClient(opts); } -export type { - CreateTRPCClientOptions, - TRPCRequestOptions, -} from './internals/TRPCUntypedClient'; +export type { CreateTRPCClientOptions } from './internals/TRPCUntypedClient'; export { TRPCUntypedClient } from './internals/TRPCUntypedClient'; diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 6fe739d5bd6..982cdb4eb89 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -5,6 +5,7 @@ export * from './createTRPCClient'; export * from './getFetch'; export * from './TRPCClientError'; export * from './links'; +export * from './createTRPCClientOptions'; export { /** diff --git a/packages/client/src/internals/TRPCUntypedClient.ts b/packages/client/src/internals/TRPCUntypedClient.ts index f7c93010e0f..bbca6c93b63 100644 --- a/packages/client/src/internals/TRPCUntypedClient.ts +++ b/packages/client/src/internals/TRPCUntypedClient.ts @@ -6,26 +6,21 @@ import { observableToPromise, share } from '@trpc/server/observable'; import type { AnyRouter, InferrableClientTypes, + ProcedureType, TypeError, } from '@trpc/server/unstable-core-do-not-import'; +import type { TRPCDecoratedClientOptions } from '../createTRPCClientOptions'; import { createChain } from '../links/internals/createChain'; import type { OperationContext, OperationLink, TRPCClientRuntime, TRPCLink, + TRPCLinkDecoration, + TRPCRequestOptions, } from '../links/types'; import { TRPCClientError } from '../TRPCClientError'; -type TRPCType = 'mutation' | 'query' | 'subscription'; -export interface TRPCRequestOptions { - /** - * Pass additional context to links - */ - context?: OperationContext; - signal?: AbortSignal; -} - export interface TRPCSubscriptionObserver { onStarted: () => void; onData: (value: TValue) => void; @@ -35,8 +30,8 @@ export interface TRPCSubscriptionObserver { } /** @internal */ -export type CreateTRPCClientOptions = { - links: TRPCLink[]; +export type CreateTRPCClientOptions = { + links: TRPCLink[]; transformer?: TypeError<'The transformer property has moved to httpLink/httpBatchLink/wsLink'>; }; @@ -51,12 +46,19 @@ export type UntypedClientProperties = | 'runtime' | 'subscription'; -export class TRPCUntypedClient { - private readonly links: OperationLink[]; +export class TRPCUntypedClient< + TRoot extends InferrableClientTypes, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, +> { + private readonly links: OperationLink[]; public readonly runtime: TRPCClientRuntime; private requestId: number; - constructor(opts: CreateTRPCClientOptions) { + constructor( + opts: + | CreateTRPCClientOptions + | TRPCDecoratedClientOptions, + ) { this.requestId = 0; this.runtime = {}; @@ -65,19 +67,19 @@ export class TRPCUntypedClient { this.links = opts.links.map((link) => link(this.runtime)); } - private $request({ + public $request({ type, input, path, context = {}, }: { - type: TRPCType; + type: ProcedureType; input: TInput; path: string; context?: OperationContext; }) { - const chain$ = createChain({ - links: this.links as OperationLink[], + const chain$ = createChain({ + links: this.links as OperationLink[], op: { id: ++this.requestId, type, @@ -89,13 +91,13 @@ export class TRPCUntypedClient { return chain$.pipe(share()); } private requestAsPromise(opts: { - type: TRPCType; + type: ProcedureType; input: TInput; path: string; context?: OperationContext; signal?: AbortSignal; }): Promise { - const req$ = this.$request(opts); + const req$ = this.$request(opts); type TValue = inferObservableValue; const { promise, abort } = observableToPromise(req$); @@ -115,29 +117,31 @@ export class TRPCUntypedClient { } public query(path: string, input?: unknown, opts?: TRPCRequestOptions) { return this.requestAsPromise({ + ...opts, type: 'query', path, input, - context: opts?.context, - signal: opts?.signal, }); } - public mutation(path: string, input?: unknown, opts?: TRPCRequestOptions) { + public mutation( + path: string, + input?: unknown, + opts?: TRPCRequestOptions, + ) { return this.requestAsPromise({ + ...opts, type: 'mutation', path, input, - context: opts?.context, - signal: opts?.signal, }); } public subscription( path: string, input: unknown, opts: Partial< - TRPCSubscriptionObserver> - > & - TRPCRequestOptions, + TRPCSubscriptionObserver> & + TRPCRequestOptions + >, ): Unsubscribable { const observable$ = this.$request({ type: 'subscription', diff --git a/packages/client/src/links/httpLink.ts b/packages/client/src/links/httpLink.ts index 342a97451e0..041321786ed 100644 --- a/packages/client/src/links/httpLink.ts +++ b/packages/client/src/links/httpLink.ts @@ -1,6 +1,6 @@ import { observable } from '@trpc/server/observable'; import type { - AnyRootTypes, + AnyClientTypes, AnyRouter, } from '@trpc/server/unstable-core-do-not-import'; import { transformResult } from '@trpc/server/unstable-core-do-not-import'; @@ -16,7 +16,7 @@ import { } from './internals/httpUtils'; import type { HTTPHeaders, Operation, TRPCLink } from './types'; -export type HTTPLinkOptions = +export type HTTPLinkOptions = HTTPLinkBaseOptions & { /** * Headers to be set on outgoing requests or a callback that of said headers diff --git a/packages/client/src/links/internals/createChain.test.ts b/packages/client/src/links/internals/createChain.test.ts index c4e4b8984cd..2f6cf5d116d 100644 --- a/packages/client/src/links/internals/createChain.test.ts +++ b/packages/client/src/links/internals/createChain.test.ts @@ -4,7 +4,7 @@ import { createChain } from './createChain'; describe('chain', () => { test('trivial', () => { - const result$ = createChain({ + const result$ = createChain({ links: [ ({ next, op }) => { return observable((observer) => { @@ -45,7 +45,7 @@ describe('chain', () => { expect(next).toHaveBeenCalledTimes(1); }); test('multiple responses', () => { - const result$ = createChain({ + const result$ = createChain({ links: [ ({ next, op }) => { return observable((observer) => { diff --git a/packages/client/src/links/internals/createChain.ts b/packages/client/src/links/internals/createChain.ts index 7a68400a022..220c5c562a7 100644 --- a/packages/client/src/links/internals/createChain.ts +++ b/packages/client/src/links/internals/createChain.ts @@ -6,15 +6,12 @@ import type { OperationResultObservable, } from '../types'; +type FIXME = any; /** @internal */ -export function createChain< - TRouter extends AnyRouter, - TInput = unknown, - TOutput = unknown, ->(opts: { - links: OperationLink[]; - op: Operation; -}): OperationResultObservable { +export function createChain(opts: { + links: OperationLink[]; + op: Operation; +}): OperationResultObservable { return observable((observer) => { function execute(index = 0, op = opts.op) { const next = opts.links[index]; @@ -24,7 +21,7 @@ export function createChain< ); } const subscription = next({ - op, + op: op as FIXME, next(nextOp) { const nextObserver = execute(index + 1, nextOp); diff --git a/packages/client/src/links/internals/dedupeLink.test.ts b/packages/client/src/links/internals/dedupeLink.test.ts index f9ed947c551..9d514b24d85 100644 --- a/packages/client/src/links/internals/dedupeLink.test.ts +++ b/packages/client/src/links/internals/dedupeLink.test.ts @@ -8,7 +8,7 @@ import { dedupeLink } from './dedupeLink'; test('dedupeLink', async () => { const endingLinkTriggered = vi.fn(); const timerTriggered = vi.fn(); - const links: OperationLink[] = [ + const links: OperationLink[] = [ // "dedupe link" dedupeLink()(null as any), ({ op }) => { @@ -33,7 +33,7 @@ test('dedupeLink', async () => { }, ]; { - const call1 = createChain({ + const call1 = createChain({ links, op: { type: 'query', @@ -44,7 +44,7 @@ test('dedupeLink', async () => { }, }); - const call2 = createChain({ + const call2 = createChain({ links, op: { type: 'query', @@ -70,7 +70,7 @@ test('dedupeLink', async () => { test('dedupe - cancel one does not cancel the other', async () => { const endingLinkTriggered = vi.fn(); const timerTriggered = vi.fn(); - const links: OperationLink[] = [ + const links: OperationLink[] = [ // "dedupe link" dedupeLink()(null as any), ({ op }) => { @@ -96,7 +96,7 @@ test('dedupe - cancel one does not cancel the other', async () => { ]; { - const call1 = createChain({ + const call1 = createChain({ links, op: { type: 'query', @@ -107,7 +107,7 @@ test('dedupe - cancel one does not cancel the other', async () => { }, }); - const call2 = createChain({ + const call2 = createChain({ links, op: { type: 'query', diff --git a/packages/client/src/links/internals/httpUtils.ts b/packages/client/src/links/internals/httpUtils.ts index 48534bd326a..c90b362eef8 100644 --- a/packages/client/src/links/internals/httpUtils.ts +++ b/packages/client/src/links/internals/httpUtils.ts @@ -1,4 +1,5 @@ import type { + AnyClientTypes, AnyRootTypes, CombinedDataTransformer, ProcedureType, @@ -23,7 +24,7 @@ import type { HTTPHeaders, PromiseAndCancel } from '../types'; * @internal */ export type HTTPLinkBaseOptions< - TRoot extends Pick, + TRoot extends Pick, > = { url: string | URL; /** diff --git a/packages/client/src/links/splitLink.test.ts b/packages/client/src/links/splitLink.test.ts index 5b1a3952255..539902ebb63 100644 --- a/packages/client/src/links/splitLink.test.ts +++ b/packages/client/src/links/splitLink.test.ts @@ -15,7 +15,7 @@ test('splitLink', () => { observable(() => { httpLinkSpy(); }); - const links: OperationLink[] = [ + const links: OperationLink[] = [ // "dedupe link" splitLink({ condition(op) { diff --git a/packages/client/src/links/types.ts b/packages/client/src/links/types.ts index e1f2c35693e..6d87104ba44 100644 --- a/packages/client/src/links/types.ts +++ b/packages/client/src/links/types.ts @@ -1,6 +1,8 @@ import type { Observable, Observer } from '@trpc/server/observable'; import type { InferrableClientTypes, + Overwrite, + ProcedureType, TRPCResultMessage, TRPCSuccessResponse, } from '@trpc/server/unstable-core-do-not-import'; @@ -28,13 +30,24 @@ export interface OperationContext extends Record {} /** * @internal */ -export type Operation = { +export type Operation< + TInput = unknown, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, +> = { id: number; - type: 'mutation' | 'query' | 'subscription'; input: TInput; path: string; - context: OperationContext; -}; +} & ( + | ({ + type: 'query'; + } & TRPCRequestOptions) + | ({ + type: 'mutation'; + } & TRPCRequestOptions) + | ({ + type: 'subscription'; + } & TRPCRequestOptions) +); interface HeadersInitEsque { [Symbol.iterator](): IterableIterator<[string, string]>; @@ -75,7 +88,7 @@ export interface OperationResultEnvelope { */ export type OperationResultObservable< TInferrable extends InferrableClientTypes, - TOutput, + TOutput = unknown, > = Observable, TRPCClientError>; /** @@ -91,18 +104,52 @@ export type OperationResultObserver< */ export type OperationLink< TInferrable extends InferrableClientTypes, - TInput = unknown, - TOutput = unknown, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, > = (opts: { - op: Operation; + op: Operation; next: ( - op: Operation, - ) => OperationResultObservable; -}) => OperationResultObservable; + op: Operation, + ) => OperationResultObservable; +}) => OperationResultObservable; + +/** + * @internal + * Links can decorate the stuff we use when using a tRPC client + */ +export type TRPCLinkDecoration = { + /** + * Extra params available when calling `.query(undefined, { /* here * /})` + */ + query: object; + mutation: object; + subscription: object; + /** + * Extra runtime available + */ + runtime: object; +}; + +export type TRPCLinkDecoratorObject< + TDecoration extends Partial, +> = Overwrite; /** * @public */ -export type TRPCLink = ( - opts: TRPCClientRuntime, -) => OperationLink; +export type TRPCLink< + TInferrable extends InferrableClientTypes, + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, +> = ( + opts: TRPCClientRuntime & Partial, +) => OperationLink; + +export type TRPCRequestOptions< + TDecoration extends TRPCLinkDecoration = TRPCLinkDecoration, + TType extends ProcedureType = ProcedureType, +> = { + /** + * Pass additional context to links + */ + context: OperationContext; + signal?: AbortSignal; +} & Partial; diff --git a/packages/next/src/app-dir/create-action-hook.tsx b/packages/next/src/app-dir/create-action-hook.tsx index a541f7e75bc..27f73e470bd 100644 --- a/packages/next/src/app-dir/create-action-hook.tsx +++ b/packages/next/src/app-dir/create-action-hook.tsx @@ -14,7 +14,6 @@ import type { inferClientTypes, InferrableClientTypes, MaybePromise, - ProcedureOptions, Simplify, TypeError, } from '@trpc/server/unstable-core-do-not-import'; @@ -25,8 +24,8 @@ import type { ActionHandlerDef } from './shared'; import { isFormData } from './shared'; type MutationArgs = TDef['input'] extends void - ? [input?: undefined | void, opts?: ProcedureOptions] - : [input: FormData | TDef['input'], opts?: ProcedureOptions]; + ? [input?: undefined | void, opts?: TRPCRequestOptions] + : [input: FormData | TDef['input'], opts?: TRPCRequestOptions]; interface UseTRPCActionBaseResult { mutate: (...args: MutationArgs) => void; diff --git a/packages/next/src/app-dir/shared.ts b/packages/next/src/app-dir/shared.ts index 410937ff458..9d38ec8192e 100644 --- a/packages/next/src/app-dir/shared.ts +++ b/packages/next/src/app-dir/shared.ts @@ -1,6 +1,7 @@ import type { + ClientProcedureCall, CreateTRPCClientOptions, - Resolver, + TRPCLinkDecoration, TRPCUntypedClient, } from '@trpc/client'; import type { @@ -27,12 +28,16 @@ export type UseProcedureRecord< ? $Value extends RouterRecord ? UseProcedureRecord : $Value extends AnyQueryProcedure - ? Resolver<{ - input: inferProcedureInput<$Value>; - output: inferTransformedProcedureOutput; - errorShape: TRoot['errorShape']; - transformer: TRoot['transformer']; - }> + ? ClientProcedureCall< + { + input: inferProcedureInput<$Value>; + output: inferTransformedProcedureOutput; + errorShape: TRoot['errorShape']; + transformer: TRoot['transformer']; + }, + 'query', + TRPCLinkDecoration + > : never : never; }; diff --git a/packages/next/src/app-dir/types.ts b/packages/next/src/app-dir/types.ts index 7ec9c2cb31c..2dfef6eb902 100644 --- a/packages/next/src/app-dir/types.ts +++ b/packages/next/src/app-dir/types.ts @@ -1,4 +1,4 @@ -import type { Resolver } from '@trpc/client'; +import type { ClientProcedureCall, TRPCLinkDecoration } from '@trpc/client'; import type { AnyProcedure, AnyRootTypes, @@ -20,7 +20,7 @@ export type DecorateProcedureServer< TDef extends ResolverDef, > = TType extends 'query' ? { - query: Resolver; + query: ClientProcedureCall; revalidate: ( input?: TDef['input'], ) => Promise< @@ -29,12 +29,10 @@ export type DecorateProcedureServer< } : TType extends 'mutation' ? { - mutate: Resolver; + mutate: ClientProcedureCall; } : TType extends 'subscription' - ? { - subscribe: Resolver; - } + ? never : never; export type NextAppDirDecorateRouterRecord< diff --git a/packages/next/src/withTRPC.tsx b/packages/next/src/withTRPC.tsx index edf55da2ad3..d57f57591ad 100644 --- a/packages/next/src/withTRPC.tsx +++ b/packages/next/src/withTRPC.tsx @@ -152,7 +152,7 @@ export function withTRPC< if (opts.ssr) { opts.ssrPrepass({ - parent: opts, + parent: opts as any, AppOrPage, WithTRPC, }); diff --git a/packages/server/src/@trpc/server/index.ts b/packages/server/src/@trpc/server/index.ts index 933f708f026..1aa3f431416 100644 --- a/packages/server/src/@trpc/server/index.ts +++ b/packages/server/src/@trpc/server/index.ts @@ -31,7 +31,6 @@ export { type AnyQueryProcedure as AnyTRPCQueryProcedure, type RouterRecord as TRPCRouterRecord, type AnySubscriptionProcedure as AnyTRPCSubscriptionProcedure, - type ProcedureOptions as TRPCProcedureOptions, } from '../../unstable-core-do-not-import'; export type { diff --git a/packages/server/src/observable/operators.ts b/packages/server/src/observable/operators.ts index ae00b2b9b15..a1499242105 100644 --- a/packages/server/src/observable/operators.ts +++ b/packages/server/src/observable/operators.ts @@ -44,6 +44,7 @@ export function share( } subscription = source.subscribe({ next(value) { + console.log('share got', value); for (const observer of observers) { observer.next?.(value); } diff --git a/packages/server/src/unstable-core-do-not-import/initTRPC.ts b/packages/server/src/unstable-core-do-not-import/initTRPC.ts index 817a63a0524..ce9837b4494 100644 --- a/packages/server/src/unstable-core-do-not-import/initTRPC.ts +++ b/packages/server/src/unstable-core-do-not-import/initTRPC.ts @@ -79,7 +79,9 @@ class TRPCBuilder { const config: RootConfig<$Root> = { transformer: getDataTransformer(opts?.transformer ?? defaultTransformer), - isDev: opts?.isDev ?? globalThis.process?.env?.NODE_ENV !== 'production', + isDev: + // eslint-disable-next-line @typescript-eslint/dot-notation + opts?.isDev ?? globalThis.process?.env['NODE_ENV'] !== 'production', allowOutsideOfServer: opts?.allowOutsideOfServer ?? false, errorFormatter: opts?.errorFormatter ?? defaultFormatter, isServer: opts?.isServer ?? isServerDefault, diff --git a/packages/server/src/unstable-core-do-not-import/procedure.ts b/packages/server/src/unstable-core-do-not-import/procedure.ts index 83ee8f1213f..e9158e8cbee 100644 --- a/packages/server/src/unstable-core-do-not-import/procedure.ts +++ b/packages/server/src/unstable-core-do-not-import/procedure.ts @@ -6,19 +6,6 @@ export const procedureTypes = ['query', 'mutation', 'subscription'] as const; */ export type ProcedureType = (typeof procedureTypes)[number]; -type ClientContext = Record; - -/** - * @internal - */ -export interface ProcedureOptions { - /** - * Client-side context - */ - context?: ClientContext; - signal?: AbortSignal; -} - interface BuiltProcedureDef { input: unknown; output: unknown;