@trpc/client
Advanced tools
Comparing version 11.0.0-alpha-tmp.234 to 11.0.0-alpha-tmp-01-21-trpc-monorepo-prebuilt.739
{ | ||
"bundleSize": 47808, | ||
"bundleOrigSize": 65642, | ||
"bundleReduction": 27.17, | ||
"bundleSize": 66027, | ||
"bundleOrigSize": 83146, | ||
"bundleReduction": 20.59, | ||
"modules": [ | ||
{ | ||
"id": "/src/links/wsLink.ts", | ||
"size": 11967, | ||
"origSize": 13296, | ||
"size": 18016, | ||
"origSize": 20084, | ||
"renderedExports": [ | ||
@@ -16,67 +16,75 @@ "createWSClient", | ||
"dependents": [], | ||
"percent": 25.03, | ||
"reduction": 10 | ||
"percent": 27.29, | ||
"reduction": 10.3 | ||
}, | ||
{ | ||
"id": "/src/links/loggerLink.ts", | ||
"size": 5235, | ||
"origSize": 6444, | ||
"id": "/src/links/httpSubscriptionLink.ts", | ||
"size": 7788, | ||
"origSize": 7568, | ||
"renderedExports": [ | ||
"loggerLink" | ||
"unstable_httpSubscriptionLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 10.95, | ||
"reduction": 18.76 | ||
"percent": 11.8, | ||
"reduction": 0 | ||
}, | ||
{ | ||
"id": "/src/internals/dataLoader.ts", | ||
"size": 4409, | ||
"origSize": 4761, | ||
"id": "/src/links/httpBatchStreamLink.ts", | ||
"size": 6006, | ||
"origSize": 6284, | ||
"renderedExports": [ | ||
"dataLoader" | ||
"unstable_httpBatchStreamLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/internals/createHTTPBatchLink.ts" | ||
"dependents": [], | ||
"percent": 9.1, | ||
"reduction": 4.42 | ||
}, | ||
{ | ||
"id": "/src/links/loggerLink.ts", | ||
"size": 5596, | ||
"origSize": 6946, | ||
"renderedExports": [ | ||
"loggerLink" | ||
], | ||
"percent": 9.22, | ||
"reduction": 7.39 | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 8.48, | ||
"reduction": 19.44 | ||
}, | ||
{ | ||
"id": "/src/links/internals/parseJSONStream.ts", | ||
"size": 4007, | ||
"origSize": 4923, | ||
"id": "/src/internals/dataLoader.ts", | ||
"size": 4084, | ||
"origSize": 4328, | ||
"renderedExports": [ | ||
"parseJSONStream", | ||
"streamingJsonHttpRequester" | ||
"dataLoader" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpBatchStreamLink.ts" | ||
], | ||
"percent": 8.38, | ||
"reduction": 18.61 | ||
"percent": 6.19, | ||
"reduction": 5.64 | ||
}, | ||
{ | ||
"id": "/src/internals/TRPCUntypedClient.ts", | ||
"size": 3250, | ||
"origSize": 6125, | ||
"id": "/src/links/httpBatchLink.ts", | ||
"size": 3937, | ||
"origSize": 4170, | ||
"renderedExports": [ | ||
"TRPCUntypedClient" | ||
"httpBatchLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/createTRPCUntypedClient.ts", | ||
"/src/createTRPCClient.ts" | ||
], | ||
"percent": 6.8, | ||
"reduction": 46.94 | ||
"dependents": [], | ||
"percent": 5.96, | ||
"reduction": 5.59 | ||
}, | ||
{ | ||
"id": "/src/links/internals/httpUtils.ts", | ||
"size": 3243, | ||
"origSize": 5267, | ||
"size": 3692, | ||
"origSize": 5873, | ||
"renderedExports": [ | ||
"resolveHTTPLinkOptions", | ||
"getInput", | ||
"getUrl", | ||
@@ -90,45 +98,41 @@ "getBody", | ||
"dependents": [ | ||
"/src/links/internals/parseJSONStream.ts", | ||
"/src/links/httpFormDataLink.ts", | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpLink.ts", | ||
"/src/links/internals/createHTTPBatchLink.ts", | ||
"/src/links/httpBatchLink.ts" | ||
"/src/links/httpBatchStreamLink.ts", | ||
"/src/links/httpSubscriptionLink.ts" | ||
], | ||
"percent": 6.78, | ||
"reduction": 38.43 | ||
"percent": 5.59, | ||
"reduction": 37.14 | ||
}, | ||
{ | ||
"id": "/src/links/internals/createHTTPBatchLink.ts", | ||
"size": 2912, | ||
"origSize": 3484, | ||
"id": "/src/links/httpLink.ts", | ||
"size": 3179, | ||
"origSize": 3707, | ||
"renderedExports": [ | ||
"createHTTPBatchLink" | ||
"httpLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpBatchStreamLink.ts", | ||
"/src/links/httpBatchLink.ts" | ||
], | ||
"percent": 6.09, | ||
"reduction": 16.42 | ||
"dependents": [], | ||
"percent": 4.81, | ||
"reduction": 14.24 | ||
}, | ||
{ | ||
"id": "/src/links/httpLink.ts", | ||
"size": 2172, | ||
"origSize": 2498, | ||
"id": "/src/internals/TRPCUntypedClient.ts", | ||
"size": 3158, | ||
"origSize": 4578, | ||
"renderedExports": [ | ||
"httpLinkFactory", | ||
"httpLink" | ||
"TRPCUntypedClient" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpFormDataLink.ts" | ||
"/src/createTRPCUntypedClient.ts", | ||
"/src/createTRPCClient.ts" | ||
], | ||
"percent": 4.54, | ||
"reduction": 13.05 | ||
"percent": 4.78, | ||
"reduction": 31.02 | ||
}, | ||
{ | ||
"id": "/src/TRPCClientError.ts", | ||
"size": 1809, | ||
"origSize": 3226, | ||
"size": 2787, | ||
"origSize": 3564, | ||
"renderedExports": [ | ||
@@ -139,57 +143,46 @@ "TRPCClientError" | ||
"dependents": [ | ||
"/src/internals/TRPCUntypedClient.ts", | ||
"/src/index.ts", | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpLink.ts", | ||
"/src/links/wsLink.ts", | ||
"/src/links/internals/httpUtils.ts", | ||
"/src/links/internals/createHTTPBatchLink.ts" | ||
"/src/links/httpBatchStreamLink.ts", | ||
"/src/links/httpSubscriptionLink.ts", | ||
"/src/internals/TRPCUntypedClient.ts" | ||
], | ||
"percent": 3.78, | ||
"reduction": 43.92 | ||
"percent": 4.22, | ||
"reduction": 21.8 | ||
}, | ||
{ | ||
"id": "/src/shared/transformResult.ts", | ||
"size": 1558, | ||
"origSize": 2208, | ||
"id": "/src/links/retryLink.ts", | ||
"size": 2194, | ||
"origSize": 2702, | ||
"renderedExports": [ | ||
"transformResult" | ||
"retryLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpLink.ts", | ||
"/src/links/wsLink.ts", | ||
"/src/shared/index.ts", | ||
"/src/links/internals/createHTTPBatchLink.ts" | ||
], | ||
"percent": 3.26, | ||
"reduction": 29.44 | ||
"dependents": [], | ||
"percent": 3.32, | ||
"reduction": 18.8 | ||
}, | ||
{ | ||
"id": "/src/links/httpBatchStreamLink.ts", | ||
"size": 1309, | ||
"origSize": 1971, | ||
"id": "/src/internals/signals.ts", | ||
"size": 1188, | ||
"origSize": 1236, | ||
"renderedExports": [ | ||
"unstable_httpBatchStreamLink" | ||
"allAbortSignals", | ||
"raceAbortSignals" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 2.74, | ||
"reduction": 33.59 | ||
}, | ||
{ | ||
"id": "/src/links/httpBatchLink.ts", | ||
"size": 1185, | ||
"origSize": 1365, | ||
"renderedExports": [ | ||
"httpBatchLink" | ||
"dependents": [ | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpBatchStreamLink.ts", | ||
"/src/links/httpSubscriptionLink.ts" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 2.48, | ||
"reduction": 13.19 | ||
"percent": 1.8, | ||
"reduction": 3.88 | ||
}, | ||
{ | ||
"id": "/src/createTRPCClient.ts", | ||
"size": 1150, | ||
"origSize": 4300, | ||
"size": 1185, | ||
"origSize": 4701, | ||
"renderedExports": [ | ||
@@ -205,21 +198,9 @@ "clientCallTypeToProcedureType", | ||
], | ||
"percent": 2.41, | ||
"reduction": 73.26 | ||
"percent": 1.79, | ||
"reduction": 74.79 | ||
}, | ||
{ | ||
"id": "/src/links/httpFormDataLink.ts", | ||
"size": 709, | ||
"origSize": 727, | ||
"renderedExports": [ | ||
"experimental_formDataLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 1.48, | ||
"reduction": 2.48 | ||
}, | ||
{ | ||
"id": "/src/links/internals/createChain.ts", | ||
"size": 690, | ||
"origSize": 977, | ||
"origSize": 1026, | ||
"renderedExports": [ | ||
@@ -230,12 +211,12 @@ "createChain" | ||
"dependents": [ | ||
"/src/internals/TRPCUntypedClient.ts", | ||
"/src/links/splitLink.ts" | ||
"/src/links/splitLink.ts", | ||
"/src/internals/TRPCUntypedClient.ts" | ||
], | ||
"percent": 1.44, | ||
"reduction": 29.38 | ||
"percent": 1.05, | ||
"reduction": 32.75 | ||
}, | ||
{ | ||
"id": "/src/links/splitLink.ts", | ||
"size": 598, | ||
"origSize": 1066, | ||
"size": 610, | ||
"origSize": 1108, | ||
"renderedExports": [ | ||
@@ -246,52 +227,82 @@ "splitLink" | ||
"dependents": [], | ||
"percent": 1.25, | ||
"reduction": 43.9 | ||
"percent": 0.92, | ||
"reduction": 44.95 | ||
}, | ||
{ | ||
"id": "/src/links/internals/getTextDecoder.ts", | ||
"size": 553, | ||
"origSize": 629, | ||
"id": "/src/internals/transformer.ts", | ||
"size": 565, | ||
"origSize": 1697, | ||
"renderedExports": [ | ||
"getTextDecoder" | ||
"getTransformer" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpBatchStreamLink.ts" | ||
"/src/unstable-internals.ts" | ||
], | ||
"percent": 1.16, | ||
"reduction": 12.08 | ||
"percent": 0.86, | ||
"reduction": 66.71 | ||
}, | ||
{ | ||
"id": "/src/internals/getAbortController.ts", | ||
"size": 542, | ||
"origSize": 670, | ||
"id": "/src/getFetch.ts", | ||
"size": 428, | ||
"origSize": 644, | ||
"renderedExports": [ | ||
"getAbortController" | ||
"getFetch" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/index.ts", | ||
"/src/links/internals/httpUtils.ts" | ||
], | ||
"percent": 1.13, | ||
"reduction": 19.1 | ||
"percent": 0.65, | ||
"reduction": 33.54 | ||
}, | ||
{ | ||
"id": "/src/getFetch.ts", | ||
"size": 428, | ||
"origSize": 639, | ||
"id": "/src/links/internals/contentTypes.ts", | ||
"size": 330, | ||
"origSize": 389, | ||
"renderedExports": [ | ||
"getFetch" | ||
"isOctetType", | ||
"isFormData", | ||
"isNonJsonSerializable" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 0.5, | ||
"reduction": 15.17 | ||
}, | ||
{ | ||
"id": "/src/internals/inputWithTrackedEventId.ts", | ||
"size": 254, | ||
"origSize": 273, | ||
"renderedExports": [ | ||
"inputWithTrackedEventId" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/index.ts", | ||
"/src/links/internals/httpUtils.ts" | ||
"/src/links/httpSubscriptionLink.ts", | ||
"/src/links/retryLink.ts" | ||
], | ||
"percent": 0.9, | ||
"reduction": 33.02 | ||
"percent": 0.38, | ||
"reduction": 6.96 | ||
}, | ||
{ | ||
"id": "/src/links/internals/urlWithConnectionParams.ts", | ||
"size": 240, | ||
"origSize": 1016, | ||
"renderedExports": [ | ||
"resultOf" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/wsLink.ts", | ||
"/src/links/httpSubscriptionLink.ts" | ||
], | ||
"percent": 0.36, | ||
"reduction": 76.38 | ||
}, | ||
{ | ||
"id": "/src/createTRPCUntypedClient.ts", | ||
"size": 82, | ||
"origSize": 491, | ||
"size": 100, | ||
"origSize": 574, | ||
"renderedExports": [ | ||
@@ -304,4 +315,4 @@ "createTRPCUntypedClient" | ||
], | ||
"percent": 0.17, | ||
"reduction": 83.3 | ||
"percent": 0.15, | ||
"reduction": 82.58 | ||
}, | ||
@@ -311,3 +322,3 @@ { | ||
"size": 0, | ||
"origSize": 540, | ||
"origSize": 588, | ||
"renderedExports": [], | ||
@@ -320,8 +331,12 @@ "removedExports": [], | ||
{ | ||
"id": "/src/shared/index.ts", | ||
"id": "/src/unstable-internals.ts", | ||
"size": 0, | ||
"origSize": 35, | ||
"origSize": 90, | ||
"renderedExports": [], | ||
"removedExports": [], | ||
"dependents": [], | ||
"dependents": [ | ||
"/src/links/wsLink.ts", | ||
"/src/links/httpSubscriptionLink.ts", | ||
"/src/links/internals/httpUtils.ts" | ||
], | ||
"percent": 0, | ||
@@ -331,3 +346,3 @@ "reduction": 100 | ||
], | ||
"moduleCount": 22 | ||
"moduleCount": 23 | ||
} |
@@ -1,7 +0,7 @@ | ||
import type { AnyMutationProcedure, AnyProcedure, AnyQueryProcedure, AnyRootConfig, AnyRouter, AnySubscriptionProcedure, ProcedureArgs, ProcedureRouterRecord, ProcedureType } from '@trpc/core'; | ||
import { inferTransformedProcedureOutput, inferTransformedSubscriptionOutput, IntersectionError } from '@trpc/core'; | ||
import type { Unsubscribable } from '@trpc/core/observable'; | ||
import { CreateTRPCClientOptions } from './createTRPCUntypedClient'; | ||
import { TRPCSubscriptionObserver, TRPCUntypedClient, UntypedClientProperties } from './internals/TRPCUntypedClient'; | ||
import { TRPCClientError } from './TRPCClientError'; | ||
import type { Unsubscribable } from '@trpc/server/observable'; | ||
import type { AnyProcedure, AnyRouter, coerceToRouterRecord, inferClientTypes, inferProcedureInput, inferTransformedProcedureOutput, IntersectionError, ProcedureOptions, ProcedureType, RouterRecord } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { CreateTRPCClientOptions } from './createTRPCUntypedClient'; | ||
import type { TRPCSubscriptionObserver, UntypedClientProperties } from './internals/TRPCUntypedClient'; | ||
import { TRPCUntypedClient } from './internals/TRPCUntypedClient'; | ||
import type { TRPCClientError } from './TRPCClientError'; | ||
/** | ||
@@ -11,14 +11,18 @@ * @public | ||
export type inferRouterClient<TRouter extends AnyRouter> = DecoratedProcedureRecord<TRouter, TRouter['_def']['record']>; | ||
type ResolverDef = { | ||
input: any; | ||
output: any; | ||
transformer: boolean; | ||
errorShape: any; | ||
}; | ||
type coerceAsyncGeneratorToIterable<T> = T extends AsyncGenerator<infer $T, infer $Return, infer $Next> ? AsyncIterable<$T, $Return, $Next> : T; | ||
/** @internal */ | ||
export type Resolver<TConfig extends AnyRootConfig, TProcedure extends AnyProcedure> = (...args: ProcedureArgs<TProcedure['_def']>) => Promise<inferTransformedProcedureOutput<TConfig, TProcedure>>; | ||
type SubscriptionResolver<TConfig extends AnyRootConfig, TProcedure extends AnyProcedure> = (...args: [ | ||
input: ProcedureArgs<TProcedure['_def']>[0], | ||
opts: Partial<TRPCSubscriptionObserver<inferTransformedSubscriptionOutput<TConfig, TProcedure>, TRPCClientError<TConfig>>> & ProcedureArgs<TProcedure['_def']>[1] | ||
]) => Unsubscribable; | ||
type DecorateProcedure<TConfig extends AnyRootConfig, TProcedure extends AnyProcedure> = TProcedure extends AnyQueryProcedure ? { | ||
query: Resolver<TConfig, TProcedure>; | ||
} : TProcedure extends AnyMutationProcedure ? { | ||
mutate: Resolver<TConfig, TProcedure>; | ||
} : TProcedure extends AnySubscriptionProcedure ? { | ||
subscribe: SubscriptionResolver<TConfig, TProcedure>; | ||
export type Resolver<TDef extends ResolverDef> = (input: TDef['input'], opts?: ProcedureOptions) => Promise<coerceAsyncGeneratorToIterable<TDef['output']>>; | ||
type SubscriptionResolver<TDef extends ResolverDef> = (input: TDef['input'], opts: Partial<TRPCSubscriptionObserver<TDef['output'], TRPCClientError<TDef>>> & ProcedureOptions) => Unsubscribable; | ||
type DecorateProcedure<TType extends ProcedureType, TDef extends ResolverDef> = TType extends 'query' ? { | ||
query: Resolver<TDef>; | ||
} : TType extends 'mutation' ? { | ||
mutate: Resolver<TDef>; | ||
} : TType extends 'subscription' ? { | ||
subscribe: SubscriptionResolver<TDef>; | ||
} : never; | ||
@@ -28,4 +32,9 @@ /** | ||
*/ | ||
type DecoratedProcedureRecord<TRouter extends AnyRouter, TProcedures extends ProcedureRouterRecord> = { | ||
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter ? DecoratedProcedureRecord<TRouter, TProcedures[TKey]['_def']['record']> : TProcedures[TKey] extends AnyProcedure ? DecorateProcedure<TRouter['_def']['_config'], TProcedures[TKey]> : never; | ||
type DecoratedProcedureRecord<TRouter extends AnyRouter, TRecord extends RouterRecord> = { | ||
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyProcedure ? DecorateProcedure<$Value['_def']['type'], { | ||
input: inferProcedureInput<$Value>; | ||
output: inferTransformedProcedureOutput<inferClientTypes<TRouter>, $Value>; | ||
errorShape: inferClientTypes<TRouter>['errorShape']; | ||
transformer: inferClientTypes<TRouter>['transformer']; | ||
}> : $Value extends RouterRecord | AnyRouter ? DecoratedProcedureRecord<TRouter, coerceToRouterRecord<$Value>> : never : never; | ||
}; | ||
@@ -37,3 +46,3 @@ /** @internal */ | ||
*/ | ||
export type CreateTRPCClient<TRouter extends AnyRouter> = inferRouterClient<TRouter> extends infer $ProcedureRecord ? UntypedClientProperties & keyof $ProcedureRecord extends never ? inferRouterClient<TRouter> : IntersectionError<UntypedClientProperties & keyof $ProcedureRecord> : never; | ||
export type CreateTRPCClient<TRouter extends AnyRouter> = inferRouterClient<TRouter> extends infer $Value ? UntypedClientProperties & keyof $Value extends never ? inferRouterClient<TRouter> : IntersectionError<UntypedClientProperties & keyof $Value> : never; | ||
/** | ||
@@ -40,0 +49,0 @@ * @internal |
@@ -1,3 +0,4 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { CreateTRPCClientOptions, TRPCUntypedClient } from './internals/TRPCUntypedClient'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { CreateTRPCClientOptions } from './internals/TRPCUntypedClient'; | ||
import { TRPCUntypedClient } from './internals/TRPCUntypedClient'; | ||
export declare function createTRPCUntypedClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCUntypedClient<TRouter>; | ||
@@ -4,0 +5,0 @@ export type { CreateTRPCClientOptions, TRPCRequestOptions, } from './internals/TRPCUntypedClient'; |
@@ -1,3 +0,3 @@ | ||
import { FetchEsque, NativeFetchEsque } from './internals/types'; | ||
import type { FetchEsque, NativeFetchEsque } from './internals/types'; | ||
export declare function getFetch(customFetchImpl?: FetchEsque | NativeFetchEsque): FetchEsque; | ||
//# sourceMappingURL=getFetch.d.ts.map |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var createTRPCUntypedClient = require('./createTRPCUntypedClient.js'); | ||
var createTRPCClient = require('./createTRPCClient.js'); | ||
var getFetch = require('./getFetch.js'); | ||
var TRPCClientError = require('./TRPCClientError.js'); | ||
var contentTypes = require('./links/internals/contentTypes.js'); | ||
var httpBatchLink = require('./links/httpBatchLink.js'); | ||
var httpBatchStreamLink = require('./links/httpBatchStreamLink.js'); | ||
var httpLink = require('./links/httpLink.js'); | ||
var loggerLink = require('./links/loggerLink.js'); | ||
var splitLink = require('./links/splitLink.js'); | ||
var wsLink = require('./links/wsLink.js'); | ||
var httpSubscriptionLink = require('./links/httpSubscriptionLink.js'); | ||
var retryLink = require('./links/retryLink.js'); | ||
var TRPCUntypedClient = require('./internals/TRPCUntypedClient.js'); | ||
var observable = require('@trpc/core/observable'); | ||
var links_splitLink = require('./splitLink-18238436.js'); | ||
var TRPCClientError = require('./TRPCClientError-67aefe1c.js'); | ||
var core = require('@trpc/core'); | ||
var httpUtils = require('./httpUtils-60af4c3d.js'); | ||
var links_httpBatchLink = require('./httpBatchLink-4653e4a1.js'); | ||
var links_httpLink = require('./links/httpLink.js'); | ||
var links_loggerLink = require('./links/loggerLink.js'); | ||
var links_wsLink = require('./links/wsLink.js'); | ||
require('./transformResult-150436c9.js'); | ||
class TRPCUntypedClient { | ||
$request({ type , input , path , context ={} }) { | ||
const chain$ = links_splitLink.createChain({ | ||
links: this.links, | ||
op: { | ||
id: ++this.requestId, | ||
type, | ||
path, | ||
input, | ||
context | ||
} | ||
}); | ||
return chain$.pipe(observable.share()); | ||
} | ||
requestAsPromise(opts) { | ||
const req$ = this.$request(opts); | ||
const { promise , abort } = observable.observableToPromise(req$); | ||
const abortablePromise = new Promise((resolve, reject)=>{ | ||
opts.signal?.addEventListener('abort', abort); | ||
promise.then((envelope)=>{ | ||
resolve(envelope.result.data); | ||
}).catch((err)=>{ | ||
reject(TRPCClientError.TRPCClientError.from(err)); | ||
}); | ||
}); | ||
return abortablePromise; | ||
} | ||
query(path, input, opts) { | ||
return this.requestAsPromise({ | ||
type: 'query', | ||
path, | ||
input, | ||
context: opts?.context, | ||
signal: opts?.signal | ||
}); | ||
} | ||
mutation(path, input, opts) { | ||
return this.requestAsPromise({ | ||
type: 'mutation', | ||
path, | ||
input, | ||
context: opts?.context, | ||
signal: opts?.signal | ||
}); | ||
} | ||
subscription(path, input, opts) { | ||
const observable$ = this.$request({ | ||
type: 'subscription', | ||
path, | ||
input, | ||
context: opts?.context | ||
}); | ||
return observable$.subscribe({ | ||
next (envelope) { | ||
if (envelope.result.type === 'started') { | ||
opts.onStarted?.(); | ||
} else if (envelope.result.type === 'stopped') { | ||
opts.onStopped?.(); | ||
} else { | ||
opts.onData?.(envelope.result.data); | ||
} | ||
}, | ||
error (err) { | ||
opts.onError?.(err); | ||
}, | ||
complete () { | ||
opts.onComplete?.(); | ||
} | ||
}); | ||
} | ||
constructor(opts){ | ||
this.requestId = 0; | ||
const combinedTransformer = (()=>{ | ||
const transformer = opts.transformer; | ||
if (!transformer) { | ||
return { | ||
input: { | ||
serialize: (data)=>data, | ||
deserialize: (data)=>data | ||
}, | ||
output: { | ||
serialize: (data)=>data, | ||
deserialize: (data)=>data | ||
} | ||
}; | ||
} | ||
if ('input' in transformer) { | ||
return opts.transformer; | ||
} | ||
return { | ||
input: transformer, | ||
output: transformer | ||
}; | ||
})(); | ||
this.runtime = { | ||
transformer: { | ||
serialize: (data)=>combinedTransformer.input.serialize(data), | ||
deserialize: (data)=>combinedTransformer.output.deserialize(data) | ||
}, | ||
combinedTransformer | ||
}; | ||
// Initialize the links | ||
this.links = opts.links.map((link)=>link(this.runtime)); | ||
} | ||
} | ||
function createTRPCUntypedClient(opts) { | ||
return new TRPCUntypedClient(opts); | ||
} | ||
const clientCallTypeMap = { | ||
query: 'query', | ||
mutate: 'mutation', | ||
subscribe: 'subscription' | ||
}; | ||
/** @internal */ const clientCallTypeToProcedureType = (clientCallType)=>{ | ||
return clientCallTypeMap[clientCallType]; | ||
}; | ||
/** | ||
* @internal | ||
*/ function createTRPCClientProxy(client) { | ||
return core.createFlatProxy((key)=>{ | ||
if (client.hasOwnProperty(key)) { | ||
return client[key]; | ||
} | ||
if (key === '__untypedClient') { | ||
return client; | ||
} | ||
return core.createRecursiveProxy(({ path , args })=>{ | ||
const pathCopy = [ | ||
key, | ||
...path | ||
]; | ||
const procedureType = clientCallTypeToProcedureType(pathCopy.pop()); | ||
const fullPath = pathCopy.join('.'); | ||
return client[procedureType](fullPath, ...args); | ||
}); | ||
}); | ||
} | ||
function createTRPCClient(opts) { | ||
const client = new TRPCUntypedClient(opts); | ||
const proxy = createTRPCClientProxy(client); | ||
return proxy; | ||
} | ||
/** | ||
* Get an untyped client from a proxy client | ||
* @internal | ||
*/ function getUntypedClient(client) { | ||
return client.__untypedClient; | ||
} | ||
function getTextDecoder(customTextDecoder) { | ||
if (customTextDecoder) { | ||
return customTextDecoder; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain | ||
if (typeof window !== 'undefined' && window.TextDecoder) { | ||
return new window.TextDecoder(); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain | ||
if (typeof globalThis !== 'undefined' && globalThis.TextDecoder) { | ||
return new globalThis.TextDecoder(); | ||
} | ||
throw new Error('No TextDecoder implementation found'); | ||
} | ||
// Stream parsing adapted from https://www.loginradius.com/blog/engineering/guest-post/http-streaming-with-nodejs-and-fetch-api/ | ||
/** | ||
* @internal | ||
* @description Take a stream of bytes and call `onLine` with | ||
* a JSON object for each line in the stream. Expected stream | ||
* format is: | ||
* ```json | ||
* {"1": {...} | ||
* ,"0": {...} | ||
* } | ||
* ``` | ||
*/ async function parseJSONStream(opts) { | ||
const parse = opts.parse ?? JSON.parse; | ||
const onLine = (line)=>{ | ||
if (opts.signal?.aborted) return; | ||
if (!line || line === '}') { | ||
return; | ||
} | ||
/** | ||
* At this point, `line` can be one of two things: | ||
* - The first line of the stream `{"2":{...}` | ||
* - A line in the middle of the stream `,"2":{...}` | ||
*/ const indexOfColon = line.indexOf(':'); | ||
const indexAsStr = line.substring(2, indexOfColon - 1); | ||
const text = line.substring(indexOfColon + 1); | ||
opts.onSingle(Number(indexAsStr), parse(text)); | ||
}; | ||
await readLines(opts.readableStream, onLine, opts.textDecoder); | ||
} | ||
/** | ||
* Handle transforming a stream of bytes into lines of text. | ||
* To avoid using AsyncIterators / AsyncGenerators, | ||
* we use a callback for each line. | ||
* | ||
* @param readableStream can be a NodeJS stream or a WebAPI stream | ||
* @param onLine will be called for every line ('\n' delimited) in the stream | ||
*/ async function readLines(readableStream, onLine, textDecoder) { | ||
let partOfLine = ''; | ||
const onChunk = (chunk)=>{ | ||
const chunkText = textDecoder.decode(chunk); | ||
const chunkLines = chunkText.split('\n'); | ||
if (chunkLines.length === 1) { | ||
partOfLine += chunkLines[0]; | ||
} else if (chunkLines.length > 1) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked on line above | ||
onLine(partOfLine + chunkLines[0]); | ||
for(let i = 1; i < chunkLines.length - 1; i++){ | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked on line above | ||
onLine(chunkLines[i]); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length doesn't change, so is necessarily > 1 | ||
partOfLine = chunkLines[chunkLines.length - 1]; | ||
} | ||
}; | ||
// we handle 2 different types of streams, this if where we figure out which one we have | ||
if ('getReader' in readableStream) { | ||
await readStandardChunks(readableStream, onChunk); | ||
} else { | ||
await readNodeChunks(readableStream, onChunk); | ||
} | ||
onLine(partOfLine); | ||
} | ||
/** | ||
* Handle NodeJS stream | ||
*/ function readNodeChunks(stream, onChunk) { | ||
return new Promise((resolve)=>{ | ||
stream.on('data', onChunk); | ||
stream.on('end', resolve); | ||
}); | ||
} | ||
/** | ||
* Handle WebAPI stream | ||
*/ async function readStandardChunks(stream, onChunk) { | ||
const reader = stream.getReader(); | ||
let readResult = await reader.read(); | ||
while(!readResult.done){ | ||
onChunk(readResult.value); | ||
readResult = await reader.read(); | ||
} | ||
} | ||
const streamingJsonHttpRequester = (opts, onSingle)=>{ | ||
const ac = opts.AbortController ? new opts.AbortController() : null; | ||
const responsePromise = httpUtils.fetchHTTPResponse({ | ||
...opts, | ||
contentTypeHeader: 'application/json', | ||
batchModeHeader: 'stream', | ||
getUrl: httpUtils.getUrl, | ||
getBody: httpUtils.getBody | ||
}, ac); | ||
const cancel = ()=>ac?.abort(); | ||
const promise = responsePromise.then(async (res)=>{ | ||
if (!res.body) throw new Error('Received response without body'); | ||
const meta = { | ||
response: res | ||
}; | ||
return parseJSONStream({ | ||
readableStream: res.body, | ||
onSingle, | ||
parse: (string)=>({ | ||
json: JSON.parse(string), | ||
meta | ||
}), | ||
signal: ac?.signal, | ||
textDecoder: opts.textDecoder | ||
}); | ||
}); | ||
return { | ||
cancel, | ||
promise | ||
}; | ||
}; | ||
const streamRequester = (requesterOpts)=>{ | ||
const textDecoder = getTextDecoder(requesterOpts.opts.textDecoder); | ||
return (batchOps, unitResolver)=>{ | ||
const path = batchOps.map((op)=>op.path).join(','); | ||
const inputs = batchOps.map((op)=>op.input); | ||
const { cancel , promise } = streamingJsonHttpRequester({ | ||
...requesterOpts, | ||
textDecoder, | ||
path, | ||
inputs, | ||
headers () { | ||
if (!requesterOpts.opts.headers) { | ||
return {}; | ||
} | ||
if (typeof requesterOpts.opts.headers === 'function') { | ||
return requesterOpts.opts.headers({ | ||
opList: batchOps | ||
}); | ||
} | ||
return requesterOpts.opts.headers; | ||
} | ||
}, (index, res)=>{ | ||
unitResolver(index, res); | ||
}); | ||
return { | ||
/** | ||
* return an empty array because the batchLoader expects an array of results | ||
* but we've already called the `unitResolver` for each of them, there's | ||
* nothing left to do here. | ||
*/ promise: promise.then(()=>[]), | ||
cancel | ||
}; | ||
}; | ||
}; | ||
const unstable_httpBatchStreamLink = links_httpBatchLink.createHTTPBatchLink(streamRequester); | ||
const formDataRequester = (opts)=>{ | ||
if (opts.type !== 'mutation') { | ||
// TODO(?) handle formdata queries | ||
throw new Error('We only handle mutations with formdata'); | ||
} | ||
return httpUtils.httpRequest({ | ||
...opts, | ||
getUrl () { | ||
return `${opts.url}/${opts.path}`; | ||
}, | ||
getBody (opts) { | ||
if (!('input' in opts)) { | ||
return undefined; | ||
} | ||
if (!(opts.input instanceof FormData)) { | ||
throw new Error('Input is not FormData'); | ||
} | ||
return opts.input; | ||
} | ||
}); | ||
}; | ||
const experimental_formDataLink = links_httpLink.httpLinkFactory({ | ||
requester: formDataRequester | ||
}); | ||
exports.splitLink = links_splitLink.splitLink; | ||
exports.createTRPCUntypedClient = createTRPCUntypedClient.createTRPCUntypedClient; | ||
exports.clientCallTypeToProcedureType = createTRPCClient.clientCallTypeToProcedureType; | ||
exports.createTRPCClient = createTRPCClient.createTRPCClient; | ||
exports.createTRPCClientProxy = createTRPCClient.createTRPCClientProxy; | ||
exports.createTRPCProxyClient = createTRPCClient.createTRPCClient; | ||
exports.getUntypedClient = createTRPCClient.getUntypedClient; | ||
exports.getFetch = getFetch.getFetch; | ||
exports.TRPCClientError = TRPCClientError.TRPCClientError; | ||
exports.getFetch = httpUtils.getFetch; | ||
exports.httpBatchLink = links_httpBatchLink.httpBatchLink; | ||
exports.httpLink = links_httpLink.httpLink; | ||
exports.httpLinkFactory = links_httpLink.httpLinkFactory; | ||
exports.loggerLink = links_loggerLink.loggerLink; | ||
exports.createWSClient = links_wsLink.createWSClient; | ||
exports.wsLink = links_wsLink.wsLink; | ||
exports.TRPCUntypedClient = TRPCUntypedClient; | ||
exports.clientCallTypeToProcedureType = clientCallTypeToProcedureType; | ||
exports.createTRPCClient = createTRPCClient; | ||
exports.createTRPCClientProxy = createTRPCClientProxy; | ||
exports.createTRPCProxyClient = createTRPCClient; | ||
exports.createTRPCUntypedClient = createTRPCUntypedClient; | ||
exports.experimental_formDataLink = experimental_formDataLink; | ||
exports.getUntypedClient = getUntypedClient; | ||
exports.unstable_httpBatchStreamLink = unstable_httpBatchStreamLink; | ||
exports.isFormData = contentTypes.isFormData; | ||
exports.isNonJsonSerializable = contentTypes.isNonJsonSerializable; | ||
exports.isOctetType = contentTypes.isOctetType; | ||
exports.httpBatchLink = httpBatchLink.httpBatchLink; | ||
exports.unstable_httpBatchStreamLink = httpBatchStreamLink.unstable_httpBatchStreamLink; | ||
exports.httpLink = httpLink.httpLink; | ||
exports.loggerLink = loggerLink.loggerLink; | ||
exports.splitLink = splitLink.splitLink; | ||
exports.createWSClient = wsLink.createWSClient; | ||
exports.wsLink = wsLink.wsLink; | ||
exports.unstable_httpSubscriptionLink = httpSubscriptionLink.unstable_httpSubscriptionLink; | ||
exports.retryLink = retryLink.retryLink; | ||
exports.TRPCUntypedClient = TRPCUntypedClient.TRPCUntypedClient; |
@@ -1,8 +0,4 @@ | ||
import { CancelFn, PromiseAndCancel } from '../links/types'; | ||
type BatchLoader<TKey, TValue> = { | ||
export type BatchLoader<TKey, TValue> = { | ||
validate: (keys: TKey[]) => boolean; | ||
fetch: (keys: TKey[], unitResolver: (index: number, value: NonNullable<TValue>) => void) => { | ||
promise: Promise<TValue[]>; | ||
cancel: CancelFn; | ||
}; | ||
fetch: (keys: TKey[]) => Promise<TValue[] | Promise<TValue>[]>; | ||
}; | ||
@@ -15,5 +11,4 @@ /** | ||
export declare function dataLoader<TKey, TValue>(batchLoader: BatchLoader<TKey, TValue>): { | ||
load: (key: TKey) => PromiseAndCancel<TValue>; | ||
load: (key: TKey) => Promise<TValue>; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=dataLoader.d.ts.map |
@@ -1,30 +0,6 @@ | ||
import { AnyRouter, CombinedDataTransformer, DataTransformerOptions, DefaultDataTransformer } from '@trpc/core'; | ||
import { Unsubscribable } from '@trpc/core/observable'; | ||
import { OperationContext, TRPCClientRuntime, TRPCLink } from '../links/types'; | ||
import type { Unsubscribable } from '@trpc/server/observable'; | ||
import type { AnyRouter, inferAsyncIterableYield, InferrableClientTypes, TypeError } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { TRPCConnectionState } from '../links/internals/subscriptions'; | ||
import type { OperationContext, TRPCClientRuntime, TRPCLink } from '../links/types'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
type CreateTRPCClientBaseOptions<TRouter extends AnyRouter> = TRouter['_def']['_config']['transformer'] extends DefaultDataTransformer ? { | ||
/** | ||
* Data transformer | ||
* | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/data-transformers | ||
**/ | ||
transformer?: 'You must set a transformer on the backend router'; | ||
} : TRouter['_def']['_config']['transformer'] extends DataTransformerOptions ? { | ||
/** | ||
* Data transformer | ||
* | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/data-transformers | ||
**/ | ||
transformer: TRouter['_def']['_config']['transformer'] extends CombinedDataTransformer ? DataTransformerOptions : TRouter['_def']['_config']['transformer']; | ||
} : { | ||
/** | ||
* Data transformer | ||
* | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/data-transformers | ||
**/ | ||
transformer?: CombinedDataTransformer; | ||
}; | ||
export interface TRPCRequestOptions { | ||
@@ -38,11 +14,15 @@ /** | ||
export interface TRPCSubscriptionObserver<TValue, TError> { | ||
onStarted: () => void; | ||
onData: (value: TValue) => void; | ||
onStarted: (opts: { | ||
context: OperationContext | undefined; | ||
}) => void; | ||
onData: (value: inferAsyncIterableYield<TValue>) => void; | ||
onError: (err: TError) => void; | ||
onStopped: () => void; | ||
onComplete: () => void; | ||
onConnectionStateChange: (state: TRPCConnectionState<TError>) => void; | ||
} | ||
/** @internal */ | ||
export type CreateTRPCClientOptions<TRouter extends AnyRouter> = CreateTRPCClientBaseOptions<TRouter> & { | ||
export type CreateTRPCClientOptions<TRouter extends InferrableClientTypes> = { | ||
links: TRPCLink<TRouter>[]; | ||
transformer?: TypeError<'The transformer property has moved to httpLink/httpBatchLink/wsLink'>; | ||
}; | ||
@@ -62,3 +42,2 @@ /** @internal */ | ||
} | ||
export {}; | ||
//# sourceMappingURL=TRPCUntypedClient.d.ts.map |
@@ -1,18 +0,2 @@ | ||
/// <reference types="node" /> | ||
export type AbortControllerEsque = new () => AbortControllerInstanceEsque; | ||
/** | ||
* Allows you to abort one or more requests. | ||
*/ | ||
export interface AbortControllerInstanceEsque { | ||
/** | ||
* The AbortSignal object associated with this object. | ||
*/ | ||
readonly signal: AbortSignal; | ||
/** | ||
* Sets this object's AbortSignal's aborted flag and signals to | ||
* any observers that the associated activity is to be aborted. | ||
*/ | ||
abort(): void; | ||
} | ||
/** | ||
* A subset of the standard fetch function type needed by tRPC internally. | ||
@@ -44,3 +28,3 @@ * @see fetch from lib.dom.d.ts | ||
*/ | ||
body?: FormData | ReadableStream | string | null; | ||
body?: FormData | string | null | Uint8Array | Blob | File; | ||
/** | ||
@@ -57,3 +41,3 @@ * Sets the request's associated headers. | ||
*/ | ||
signal?: AbortSignal | null; | ||
signal?: AbortSignal | undefined; | ||
} | ||
@@ -67,2 +51,5 @@ /** | ||
}; | ||
export type NodeJSReadableStreamEsque = { | ||
on(eventName: string | symbol, listener: (...args: any[]) => void): NodeJSReadableStreamEsque; | ||
}; | ||
/** | ||
@@ -73,3 +60,3 @@ * A subset of the standard Response properties needed by tRPC internally. | ||
export interface ResponseEsque { | ||
readonly body?: NodeJS.ReadableStream | WebReadableStreamEsque | null; | ||
readonly body?: NodeJSReadableStreamEsque | WebReadableStreamEsque | null; | ||
/** | ||
@@ -76,0 +63,0 @@ * @remarks |
@@ -1,3 +0,8 @@ | ||
import { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
export declare const httpBatchLink: <TRouter extends import("@trpc/core").AnyRouter>(opts: HTTPBatchLinkOptions) => import("./types").TRPCLink<TRouter>; | ||
import type { AnyRouter } from '@trpc/server'; | ||
import type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import type { TRPCLink } from './types'; | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpBatchLink | ||
*/ | ||
export declare function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter['_def']['_config']['$types']>): TRPCLink<TRouter>; | ||
//# sourceMappingURL=httpBatchLink.d.ts.map |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var observable = require('@trpc/server/observable'); | ||
var unstableCoreDoNotImport = require('@trpc/server/unstable-core-do-not-import'); | ||
var dataLoader = require('../internals/dataLoader.js'); | ||
var signals = require('../internals/signals.js'); | ||
var TRPCClientError = require('../TRPCClientError.js'); | ||
var httpUtils = require('./internals/httpUtils.js'); | ||
var links_httpBatchLink = require('../httpBatchLink-4653e4a1.js'); | ||
require('../httpUtils-60af4c3d.js'); | ||
require('@trpc/core/observable'); | ||
require('../transformResult-150436c9.js'); | ||
require('@trpc/core'); | ||
require('../TRPCClientError-67aefe1c.js'); | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpBatchLink | ||
*/ function httpBatchLink(opts) { | ||
const resolvedOpts = httpUtils.resolveHTTPLinkOptions(opts); | ||
const maxURLLength = opts.maxURLLength ?? Infinity; | ||
return ()=>{ | ||
const batchLoader = (type)=>{ | ||
return { | ||
validate (batchOps) { | ||
if (maxURLLength === Infinity) { | ||
// escape hatch for quick calcs | ||
return true; | ||
} | ||
const path = batchOps.map((op)=>op.path).join(','); | ||
const inputs = batchOps.map((op)=>op.input); | ||
const url = httpUtils.getUrl({ | ||
...resolvedOpts, | ||
type, | ||
path, | ||
inputs, | ||
signal: null | ||
}); | ||
return url.length <= maxURLLength; | ||
}, | ||
async fetch (batchOps) { | ||
const path = batchOps.map((op)=>op.path).join(','); | ||
const inputs = batchOps.map((op)=>op.input); | ||
const signal = signals.allAbortSignals(...batchOps.map((op)=>op.signal)); | ||
const res = await httpUtils.jsonHttpRequester({ | ||
...resolvedOpts, | ||
path, | ||
inputs, | ||
type, | ||
headers () { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
opList: batchOps | ||
}); | ||
} | ||
return opts.headers; | ||
}, | ||
signal | ||
}); | ||
const resJSON = Array.isArray(res.json) ? res.json : batchOps.map(()=>res.json); | ||
const result = resJSON.map((item)=>({ | ||
meta: res.meta, | ||
json: item | ||
})); | ||
return result; | ||
} | ||
}; | ||
}; | ||
const query = dataLoader.dataLoader(batchLoader('query')); | ||
const mutation = dataLoader.dataLoader(batchLoader('mutation')); | ||
const loaders = { | ||
query, | ||
mutation | ||
}; | ||
return ({ op })=>{ | ||
return observable.observable((observer)=>{ | ||
/* istanbul ignore if -- @preserve */ if (op.type === 'subscription') { | ||
throw new Error('Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`'); | ||
} | ||
const loader = loaders[op.type]; | ||
const promise = loader.load(op); | ||
let _res = undefined; | ||
promise.then((res)=>{ | ||
_res = res; | ||
const transformed = unstableCoreDoNotImport.transformResult(res.json, resolvedOpts.transformer.output); | ||
if (!transformed.ok) { | ||
observer.error(TRPCClientError.TRPCClientError.from(transformed.error, { | ||
meta: res.meta | ||
})); | ||
return; | ||
} | ||
observer.next({ | ||
context: res.meta, | ||
result: transformed.result | ||
}); | ||
observer.complete(); | ||
}).catch((err)=>{ | ||
observer.error(TRPCClientError.TRPCClientError.from(err, { | ||
meta: _res?.meta | ||
})); | ||
}); | ||
return ()=>{ | ||
// noop | ||
}; | ||
}); | ||
}; | ||
}; | ||
} | ||
exports.httpBatchLink = links_httpBatchLink.httpBatchLink; | ||
exports.httpBatchLink = httpBatchLink; |
@@ -1,9 +0,10 @@ | ||
import { NonEmptyArray } from '../internals/types'; | ||
import { HTTPLinkBaseOptions } from './internals/httpUtils'; | ||
import { HTTPHeaders, Operation } from './types'; | ||
export interface HTTPBatchLinkOptions extends HTTPLinkBaseOptions { | ||
import type { AnyClientTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { NonEmptyArray } from '../internals/types'; | ||
import type { HTTPLinkBaseOptions } from './internals/httpUtils'; | ||
import type { HTTPHeaders, Operation } from './types'; | ||
export type HTTPBatchLinkOptions<TRoot extends AnyClientTypes> = HTTPLinkBaseOptions<TRoot> & { | ||
maxURLLength?: number; | ||
/** | ||
* Headers to be set on outgoing requests or a callback that of said headers | ||
* @link http://trpc.io/docs/client/headers | ||
* @see http://trpc.io/docs/client/headers | ||
*/ | ||
@@ -13,3 +14,3 @@ headers?: HTTPHeaders | ((opts: { | ||
}) => HTTPHeaders | Promise<HTTPHeaders>); | ||
} | ||
}; | ||
//# sourceMappingURL=HTTPBatchLinkOptions.d.ts.map |
@@ -1,12 +0,16 @@ | ||
import { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import { TextDecoderEsque } from './internals/streamingUtils'; | ||
export interface HTTPBatchStreamLinkOptions extends HTTPBatchLinkOptions { | ||
import type { AnyRouter } from '@trpc/server'; | ||
import type { AnyRootTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import type { TRPCLink } from './types'; | ||
export type HTTPBatchStreamLinkOptions<TRoot extends AnyRootTypes> = HTTPBatchLinkOptions<TRoot> & { | ||
/** | ||
* Will default to the webAPI `TextDecoder`, | ||
* but you can use this option if your client | ||
* runtime doesn't provide it. | ||
* Maximum number of calls in a single batch request | ||
* @default Infinity | ||
*/ | ||
textDecoder?: TextDecoderEsque; | ||
} | ||
export declare const unstable_httpBatchStreamLink: <TRouter extends import("@trpc/core").AnyRouter>(opts: HTTPBatchStreamLinkOptions) => import("./types").TRPCLink<TRouter>; | ||
maxItems?: number; | ||
}; | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpBatchStreamLink | ||
*/ | ||
export declare function unstable_httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchStreamLinkOptions<TRouter['_def']['_config']['$types']>): TRPCLink<TRouter>; | ||
//# sourceMappingURL=httpBatchStreamLink.d.ts.map |
@@ -1,8 +0,8 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { HTTPLinkBaseOptions, Requester } from './internals/httpUtils'; | ||
import { HTTPHeaders, Operation, TRPCLink } from './types'; | ||
export interface HTTPLinkOptions extends HTTPLinkBaseOptions { | ||
import type { AnyClientTypes, AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { HTTPLinkBaseOptions } from './internals/httpUtils'; | ||
import { type HTTPHeaders, type Operation, type TRPCLink } from './types'; | ||
export type HTTPLinkOptions<TRoot extends AnyClientTypes> = HTTPLinkBaseOptions<TRoot> & { | ||
/** | ||
* Headers to be set on outgoing requests or a callback that of said headers | ||
* @link http://trpc.io/docs/client/headers | ||
* @see http://trpc.io/docs/client/headers | ||
*/ | ||
@@ -12,10 +12,7 @@ headers?: HTTPHeaders | ((opts: { | ||
}) => HTTPHeaders | Promise<HTTPHeaders>); | ||
} | ||
export declare function httpLinkFactory(factoryOpts: { | ||
requester: Requester; | ||
}): <TRouter extends AnyRouter>(opts: HTTPLinkOptions) => TRPCLink<TRouter>; | ||
}; | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpLink | ||
*/ | ||
export declare const httpLink: <TRouter extends AnyRouter>(opts: HTTPLinkOptions) => TRPCLink<TRouter>; | ||
export declare function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>): TRPCLink<TRouter>; | ||
//# sourceMappingURL=httpLink.d.ts.map |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var observable = require('@trpc/server/observable'); | ||
var unstableCoreDoNotImport = require('@trpc/server/unstable-core-do-not-import'); | ||
var TRPCClientError = require('../TRPCClientError.js'); | ||
var httpUtils = require('./internals/httpUtils.js'); | ||
var contentTypes = require('./internals/contentTypes.js'); | ||
var observable = require('@trpc/core/observable'); | ||
var transformResult = require('../transformResult-150436c9.js'); | ||
var TRPCClientError = require('../TRPCClientError-67aefe1c.js'); | ||
var httpUtils = require('../httpUtils-60af4c3d.js'); | ||
require('@trpc/core'); | ||
function httpLinkFactory(factoryOpts) { | ||
return (opts)=>{ | ||
const resolvedOpts = httpUtils.resolveHTTPLinkOptions(opts); | ||
return (runtime)=>({ op })=>observable.observable((observer)=>{ | ||
const { path , input , type } = op; | ||
const { promise , cancel } = factoryOpts.requester({ | ||
...resolvedOpts, | ||
runtime, | ||
type, | ||
path, | ||
input, | ||
headers () { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
op | ||
}); | ||
} | ||
return opts.headers; | ||
const universalRequester = (opts)=>{ | ||
const input = httpUtils.getInput(opts); | ||
if (contentTypes.isFormData(input)) { | ||
if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') { | ||
throw new Error('FormData is only supported for mutations'); | ||
} | ||
return httpUtils.httpRequest({ | ||
...opts, | ||
// The browser will set this automatically and include the boundary= in it | ||
contentTypeHeader: undefined, | ||
getUrl: httpUtils.getUrl, | ||
getBody: ()=>input | ||
}); | ||
} | ||
if (contentTypes.isOctetType(input)) { | ||
if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') { | ||
throw new Error('Octet type input is only supported for mutations'); | ||
} | ||
return httpUtils.httpRequest({ | ||
...opts, | ||
contentTypeHeader: 'application/octet-stream', | ||
getUrl: httpUtils.getUrl, | ||
getBody: ()=>input | ||
}); | ||
} | ||
return httpUtils.jsonHttpRequester(opts); | ||
}; | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpLink | ||
*/ function httpLink(opts) { | ||
const resolvedOpts = httpUtils.resolveHTTPLinkOptions(opts); | ||
return ()=>{ | ||
return ({ op })=>{ | ||
return observable.observable((observer)=>{ | ||
const { path, input, type } = op; | ||
/* istanbul ignore if -- @preserve */ if (type === 'subscription') { | ||
throw new Error('Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`'); | ||
} | ||
const request = universalRequester({ | ||
...resolvedOpts, | ||
type, | ||
path, | ||
input, | ||
signal: op.signal, | ||
headers () { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
}); | ||
let meta = undefined; | ||
promise.then((res)=>{ | ||
meta = res.meta; | ||
const transformed = transformResult.transformResult(res.json, runtime); | ||
if (!transformed.ok) { | ||
observer.error(TRPCClientError.TRPCClientError.from(transformed.error, { | ||
meta | ||
})); | ||
return; | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
op | ||
}); | ||
} | ||
observer.next({ | ||
context: res.meta, | ||
result: transformed.result | ||
}); | ||
observer.complete(); | ||
}).catch((cause)=>{ | ||
observer.error(TRPCClientError.TRPCClientError.from(cause, { | ||
return opts.headers; | ||
} | ||
}); | ||
let meta = undefined; | ||
request.then((res)=>{ | ||
meta = res.meta; | ||
const transformed = unstableCoreDoNotImport.transformResult(res.json, resolvedOpts.transformer.output); | ||
if (!transformed.ok) { | ||
observer.error(TRPCClientError.TRPCClientError.from(transformed.error, { | ||
meta | ||
})); | ||
return; | ||
} | ||
observer.next({ | ||
context: res.meta, | ||
result: transformed.result | ||
}); | ||
return ()=>{ | ||
cancel(); | ||
}; | ||
observer.complete(); | ||
}).catch((cause)=>{ | ||
observer.error(TRPCClientError.TRPCClientError.from(cause, { | ||
meta | ||
})); | ||
}); | ||
return ()=>{ | ||
// noop | ||
}; | ||
}); | ||
}; | ||
}; | ||
} | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpLink | ||
*/ const httpLink = httpLinkFactory({ | ||
requester: httpUtils.jsonHttpRequester | ||
}); | ||
exports.httpLink = httpLink; | ||
exports.httpLinkFactory = httpLinkFactory; |
@@ -1,3 +0,3 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { Operation, OperationLink, OperationResultObservable } from '../types'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { Operation, OperationLink, OperationResultObservable } from '../types'; | ||
/** @internal */ | ||
@@ -4,0 +4,0 @@ export declare function createChain<TRouter extends AnyRouter, TInput = unknown, TOutput = unknown>(opts: { |
@@ -1,3 +0,3 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { TRPCLink } from '../types'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { TRPCLink } from '../types'; | ||
/** | ||
@@ -4,0 +4,0 @@ * @internal used for testing |
@@ -1,10 +0,9 @@ | ||
import { ProcedureType } from '@trpc/core'; | ||
import { TRPCResponse } from '@trpc/core/rpc'; | ||
import { AbortControllerEsque, AbortControllerInstanceEsque, FetchEsque, RequestInitEsque, ResponseEsque } from '../../internals/types'; | ||
import { TextDecoderEsque } from '../internals/streamingUtils'; | ||
import { HTTPHeaders, PromiseAndCancel, TRPCClientRuntime } from '../types'; | ||
import type { AnyClientTypes, CombinedDataTransformer, Maybe, ProcedureType, TRPCAcceptHeader, TRPCResponse } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { FetchEsque, RequestInitEsque, ResponseEsque } from '../../internals/types'; | ||
import type { TransformerOptions } from '../../unstable-internals'; | ||
import type { HTTPHeaders } from '../types'; | ||
/** | ||
* @internal | ||
*/ | ||
export interface HTTPLinkBaseOptions { | ||
export type HTTPLinkBaseOptions<TRoot extends Pick<AnyClientTypes, 'transformer'>> = { | ||
url: string | URL; | ||
@@ -16,12 +15,15 @@ /** | ||
/** | ||
* Add ponyfill for AbortController | ||
* Send all requests `as POST`s requests regardless of the procedure type | ||
* The HTTP handler must separately allow overriding the method. See: | ||
* @see https://trpc.io/docs/rpc | ||
*/ | ||
AbortController?: AbortControllerEsque | null; | ||
} | ||
methodOverride?: 'POST'; | ||
} & TransformerOptions<TRoot>; | ||
export interface ResolvedHTTPLinkOptions { | ||
url: string; | ||
fetch?: FetchEsque; | ||
AbortController: AbortControllerEsque | null; | ||
transformer: CombinedDataTransformer; | ||
methodOverride?: 'POST'; | ||
} | ||
export declare function resolveHTTPLinkOptions(opts: HTTPLinkBaseOptions): ResolvedHTTPLinkOptions; | ||
export declare function resolveHTTPLinkOptions(opts: HTTPLinkBaseOptions<AnyClientTypes>): ResolvedHTTPLinkOptions; | ||
export interface HTTPResult { | ||
@@ -35,3 +37,3 @@ json: TRPCResponse; | ||
type GetInputOptions = { | ||
runtime: TRPCClientRuntime; | ||
transformer: CombinedDataTransformer; | ||
} & ({ | ||
@@ -42,5 +44,7 @@ input: unknown; | ||
}); | ||
export declare function getInput(opts: GetInputOptions): any; | ||
export type HTTPBaseRequestOptions = GetInputOptions & ResolvedHTTPLinkOptions & { | ||
type: ProcedureType; | ||
path: string; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -50,3 +54,3 @@ type GetUrl = (opts: HTTPBaseRequestOptions) => string; | ||
export type ContentOptions = { | ||
batchModeHeader?: 'stream'; | ||
trpcAcceptHeader?: TRPCAcceptHeader; | ||
contentTypeHeader?: string; | ||
@@ -60,11 +64,10 @@ getUrl: GetUrl; | ||
headers: () => HTTPHeaders | Promise<HTTPHeaders>; | ||
}) => PromiseAndCancel<HTTPResult>; | ||
}) => Promise<HTTPResult>; | ||
export declare const jsonHttpRequester: Requester; | ||
export type HTTPRequestOptions = ContentOptions & HTTPBaseRequestOptions & { | ||
headers: () => HTTPHeaders | Promise<HTTPHeaders>; | ||
TextDecoder?: TextDecoderEsque; | ||
}; | ||
export declare function fetchHTTPResponse(opts: HTTPRequestOptions, ac?: AbortControllerInstanceEsque | null): Promise<ResponseEsque>; | ||
export declare function httpRequest(opts: HTTPRequestOptions): PromiseAndCancel<HTTPResult>; | ||
export declare function fetchHTTPResponse(opts: HTTPRequestOptions): Promise<ResponseEsque>; | ||
export declare function httpRequest(opts: HTTPRequestOptions): Promise<HTTPResult>; | ||
export {}; | ||
//# sourceMappingURL=httpUtils.d.ts.map |
@@ -1,5 +0,4 @@ | ||
/// <reference lib="dom.iterable" /> | ||
import { AnyRouter } from '@trpc/core'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import { Operation, OperationResultEnvelope, TRPCLink } from './types'; | ||
import type { AnyRouter, InferrableClientTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { TRPCClientError } from '../TRPCClientError'; | ||
import type { Operation, OperationResultEnvelope, TRPCLink } from './types'; | ||
type ConsoleEsque = { | ||
@@ -9,5 +8,5 @@ log: (...args: any[]) => void; | ||
}; | ||
type EnableFnOptions<TRouter extends AnyRouter> = { | ||
type EnableFnOptions<TRouter extends InferrableClientTypes> = { | ||
direction: 'down'; | ||
result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>; | ||
result: OperationResultEnvelope<unknown, TRPCClientError<TRouter>> | TRPCClientError<TRouter>; | ||
} | (Operation & { | ||
@@ -22,3 +21,3 @@ direction: 'up'; | ||
direction: 'down'; | ||
result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>; | ||
result: OperationResultEnvelope<unknown, TRPCClientError<TRouter>> | TRPCClientError<TRouter>; | ||
elapsedMs: number; | ||
@@ -32,2 +31,3 @@ } | { | ||
type LoggerLinkFn<TRouter extends AnyRouter> = (opts: LoggerLinkFnOptions<TRouter>) => void; | ||
type ColorMode = 'ansi' | 'css' | 'none'; | ||
export interface LoggerLinkOptions<TRouter extends AnyRouter> { | ||
@@ -44,6 +44,10 @@ logger?: LoggerLinkFn<TRouter>; | ||
*/ | ||
colorMode?: 'ansi' | 'css'; | ||
colorMode?: ColorMode; | ||
/** | ||
* Include context in the log - defaults to false unless `colorMode` is 'css' | ||
*/ | ||
withContext?: boolean; | ||
} | ||
/** | ||
* @see https://trpc.io/docs/client/links/loggerLink | ||
* @see https://trpc.io/docs/v11/client/links/loggerLink | ||
*/ | ||
@@ -50,0 +54,0 @@ export declare function loggerLink<TRouter extends AnyRouter = AnyRouter>(opts?: LoggerLinkOptions<TRouter>): TRPCLink<TRouter>; |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var observable = require('@trpc/server/observable'); | ||
var observable = require('@trpc/core/observable'); | ||
/// <reference lib="dom.iterable" /> | ||
// `dom.iterable` types are explicitly required for extracting `FormData` values, | ||
// as all implementations of `Symbol.iterable` are separated from the main `dom` types. | ||
// Using triple-slash directive makes sure that it will be available, | ||
// even if end-user `tsconfig.json` omits it in the `lib` array. | ||
function isFormData(value) { | ||
@@ -65,6 +67,8 @@ if (typeof FormData === 'undefined') { | ||
function constructPartsAndArgs(opts) { | ||
const { direction , type , path , id , input } = opts; | ||
const { direction, type, withContext, path, id, input } = opts; | ||
const parts = []; | ||
const args = []; | ||
if (opts.colorMode === 'ansi') { | ||
if (opts.colorMode === 'none') { | ||
parts.push(direction === 'up' ? '>>' : '<<', type, `#${id}`, path); | ||
} else if (opts.colorMode === 'ansi') { | ||
const [lightRegular, darkRegular] = palettes.ansi.regular[type]; | ||
@@ -74,21 +78,6 @@ const [lightBold, darkBold] = palettes.ansi.bold[type]; | ||
parts.push(direction === 'up' ? lightRegular : darkRegular, direction === 'up' ? '>>' : '<<', type, direction === 'up' ? lightBold : darkBold, `#${id}`, path, reset); | ||
if (direction === 'up') { | ||
args.push({ | ||
input: opts.input | ||
}); | ||
} else { | ||
args.push({ | ||
input: opts.input, | ||
// strip context from result cause it's too noisy in terminal wihtout collapse mode | ||
result: 'result' in opts.result ? opts.result.result : opts.result, | ||
elapsedMs: opts.elapsedMs | ||
}); | ||
} | ||
return { | ||
parts, | ||
args | ||
}; | ||
} | ||
const [light, dark] = palettes.css[type]; | ||
const css = ` | ||
} else { | ||
// css color mode | ||
const [light, dark] = palettes.css[type]; | ||
const css = ` | ||
background-color: #${direction === 'up' ? light : dark}; | ||
@@ -98,8 +87,11 @@ color: ${direction === 'up' ? 'black' : 'white'}; | ||
`; | ||
parts.push('%c', direction === 'up' ? '>>' : '<<', type, `#${id}`, `%c${path}%c`, '%O'); | ||
args.push(css, `${css}; font-weight: bold;`, `${css}; font-weight: normal;`); | ||
parts.push('%c', direction === 'up' ? '>>' : '<<', type, `#${id}`, `%c${path}%c`, '%O'); | ||
args.push(css, `${css}; font-weight: bold;`, `${css}; font-weight: normal;`); | ||
} | ||
if (direction === 'up') { | ||
args.push({ | ||
args.push(withContext ? { | ||
input, | ||
context: opts.context | ||
} : { | ||
input | ||
}); | ||
@@ -111,3 +103,5 @@ } else { | ||
elapsedMs: opts.elapsedMs, | ||
context: opts.context | ||
...withContext && { | ||
context: opts.context | ||
} | ||
}); | ||
@@ -121,11 +115,12 @@ } | ||
// maybe this should be moved to it's own package | ||
const defaultLogger = ({ c =console , colorMode ='css' })=>(props)=>{ | ||
const defaultLogger = ({ c = console, colorMode = 'css', withContext })=>(props)=>{ | ||
const rawInput = props.input; | ||
const input = isFormData(rawInput) ? Object.fromEntries(rawInput) : rawInput; | ||
const { parts , args } = constructPartsAndArgs({ | ||
const { parts, args } = constructPartsAndArgs({ | ||
...props, | ||
colorMode, | ||
input | ||
input, | ||
withContext | ||
}); | ||
const fn = props.direction === 'down' && props.result && (props.result instanceof Error || 'error' in props.result.result) ? 'error' : 'log'; | ||
const fn = props.direction === 'down' && props.result && (props.result instanceof Error || 'error' in props.result.result && props.result.result.error) ? 'error' : 'log'; | ||
c[fn].apply(null, [ | ||
@@ -136,34 +131,40 @@ parts.join(' ') | ||
/** | ||
* @see https://trpc.io/docs/client/links/loggerLink | ||
* @see https://trpc.io/docs/v11/client/links/loggerLink | ||
*/ function loggerLink(opts = {}) { | ||
const { enabled =()=>true } = opts; | ||
const { enabled = ()=>true } = opts; | ||
const colorMode = opts.colorMode ?? (typeof window === 'undefined' ? 'ansi' : 'css'); | ||
const { logger =defaultLogger({ | ||
const withContext = opts.withContext ?? colorMode === 'css'; | ||
const { logger = defaultLogger({ | ||
c: opts.console, | ||
colorMode | ||
}) } = opts; | ||
colorMode, | ||
withContext | ||
}) } = opts; | ||
return ()=>{ | ||
return ({ op , next })=>{ | ||
return ({ op, next })=>{ | ||
return observable.observable((observer)=>{ | ||
// -> | ||
enabled({ | ||
if (enabled({ | ||
...op, | ||
direction: 'up' | ||
}) && logger({ | ||
...op, | ||
direction: 'up' | ||
}); | ||
})) { | ||
logger({ | ||
...op, | ||
direction: 'up' | ||
}); | ||
} | ||
const requestStartTime = Date.now(); | ||
function logResult(result) { | ||
const elapsedMs = Date.now() - requestStartTime; | ||
enabled({ | ||
if (enabled({ | ||
...op, | ||
direction: 'down', | ||
result | ||
}) && logger({ | ||
...op, | ||
direction: 'down', | ||
elapsedMs, | ||
result | ||
}); | ||
})) { | ||
logger({ | ||
...op, | ||
direction: 'down', | ||
elapsedMs, | ||
result | ||
}); | ||
} | ||
} | ||
@@ -170,0 +171,0 @@ return next(op).pipe(observable.tap({ |
@@ -1,3 +0,3 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { Operation, TRPCLink } from './types'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { Operation, TRPCLink } from './types'; | ||
export declare function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: { | ||
@@ -4,0 +4,0 @@ condition: (op: Operation) => boolean; |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var observable = require('@trpc/server/observable'); | ||
var createChain = require('./internals/createChain.js'); | ||
require('@trpc/core/observable'); | ||
var links_splitLink = require('../splitLink-18238436.js'); | ||
function asArray(value) { | ||
return Array.isArray(value) ? value : [ | ||
value | ||
]; | ||
} | ||
function splitLink(opts) { | ||
return (runtime)=>{ | ||
const yes = asArray(opts.true).map((link)=>link(runtime)); | ||
const no = asArray(opts.false).map((link)=>link(runtime)); | ||
return (props)=>{ | ||
return observable.observable((observer)=>{ | ||
const links = opts.condition(props.op) ? yes : no; | ||
return createChain.createChain({ | ||
op: props.op, | ||
links | ||
}).subscribe(observer); | ||
}); | ||
}; | ||
}; | ||
} | ||
exports.splitLink = links_splitLink.splitLink; | ||
exports.splitLink = splitLink; |
@@ -1,20 +0,10 @@ | ||
import { AnyRouter, CombinedDataTransformer, DataTransformer } from '@trpc/core'; | ||
import { Observable, Observer } from '@trpc/core/observable'; | ||
import { TRPCResultMessage, TRPCSuccessResponse } from '@trpc/core/rpc'; | ||
import { ResponseEsque } from '../internals/types'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import type { Observable, Observer } from '@trpc/server/observable'; | ||
import type { InferrableClientTypes, Maybe, TRPCResultMessage, TRPCSuccessResponse } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { ResponseEsque } from '../internals/types'; | ||
import type { TRPCClientError } from '../TRPCClientError'; | ||
import type { TRPCConnectionState } from './internals/subscriptions'; | ||
export { isNonJsonSerializable, isFormData, isOctetType, } from './internals/contentTypes'; | ||
/** | ||
* @internal | ||
*/ | ||
export type CancelFn = () => void; | ||
/** | ||
* @internal | ||
*/ | ||
export type PromiseAndCancel<TValue> = { | ||
promise: Promise<TValue>; | ||
cancel: CancelFn; | ||
}; | ||
/** | ||
* @internal | ||
*/ | ||
export interface OperationContext extends Record<string, unknown> { | ||
@@ -31,2 +21,3 @@ } | ||
context: OperationContext; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -46,4 +37,2 @@ interface HeadersInitEsque { | ||
export interface TRPCClientRuntime { | ||
transformer: DataTransformer; | ||
combinedTransformer: CombinedDataTransformer; | ||
} | ||
@@ -53,4 +42,4 @@ /** | ||
*/ | ||
export interface OperationResultEnvelope<TOutput> { | ||
result: TRPCResultMessage<TOutput>['result'] | TRPCSuccessResponse<TOutput>['result']; | ||
export interface OperationResultEnvelope<TOutput, TError> { | ||
result: TRPCResultMessage<TOutput>['result'] | TRPCSuccessResponse<TOutput>['result'] | TRPCConnectionState<TError>; | ||
context?: OperationContext; | ||
@@ -61,19 +50,18 @@ } | ||
*/ | ||
export type OperationResultObservable<TRouter extends AnyRouter, TOutput> = Observable<OperationResultEnvelope<TOutput>, TRPCClientError<TRouter>>; | ||
export type OperationResultObservable<TInferrable extends InferrableClientTypes, TOutput> = Observable<OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, TRPCClientError<TInferrable>>; | ||
/** | ||
* @internal | ||
*/ | ||
export type OperationResultObserver<TRouter extends AnyRouter, TOutput> = Observer<OperationResultEnvelope<TOutput>, TRPCClientError<TRouter>>; | ||
export type OperationResultObserver<TInferrable extends InferrableClientTypes, TOutput> = Observer<OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, TRPCClientError<TInferrable>>; | ||
/** | ||
* @internal | ||
*/ | ||
export type OperationLink<TRouter extends AnyRouter, TInput = unknown, TOutput = unknown> = (opts: { | ||
export type OperationLink<TInferrable extends InferrableClientTypes, TInput = unknown, TOutput = unknown> = (opts: { | ||
op: Operation<TInput>; | ||
next: (op: Operation<TInput>) => OperationResultObservable<TRouter, TOutput>; | ||
}) => OperationResultObservable<TRouter, TOutput>; | ||
next: (op: Operation<TInput>) => OperationResultObservable<TInferrable, TOutput>; | ||
}) => OperationResultObservable<TInferrable, TOutput>; | ||
/** | ||
* @public | ||
*/ | ||
export type TRPCLink<TRouter extends AnyRouter> = (opts: TRPCClientRuntime) => OperationLink<TRouter>; | ||
export {}; | ||
export type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -1,15 +0,13 @@ | ||
import { AnyRouter, inferRouterError, MaybePromise } from '@trpc/core'; | ||
import { Observer, UnsubscribeFn } from '@trpc/core/observable'; | ||
import { TRPCResponseMessage } from '@trpc/core/rpc'; | ||
import type { Observer, UnsubscribeFn } from '@trpc/server/observable'; | ||
import type { AnyRouter, inferClientTypes, inferRouterError, TRPCResponseMessage } from '@trpc/server/unstable-core-do-not-import'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import { Operation, TRPCLink } from './types'; | ||
import type { TransformerOptions } from '../unstable-internals'; | ||
import type { TRPCConnectionState } from './internals/subscriptions'; | ||
import { type UrlOptionsWithConnectionParams } from './internals/urlWithConnectionParams'; | ||
import type { Operation, TRPCLink } from './types'; | ||
type WSCallbackResult<TRouter extends AnyRouter, TOutput> = TRPCResponseMessage<TOutput, inferRouterError<TRouter>>; | ||
type WSCallbackObserver<TRouter extends AnyRouter, TOutput> = Observer<WSCallbackResult<TRouter, TOutput>, TRPCClientError<TRouter>>; | ||
declare const exponentialBackoff: (attemptIndex: number) => number; | ||
export interface WebSocketClientOptions { | ||
export interface WebSocketClientOptions extends UrlOptionsWithConnectionParams { | ||
/** | ||
* The URL to connect to (can be a function that returns a URL) | ||
*/ | ||
url: string | (() => MaybePromise<string>); | ||
/** | ||
* Ponyfill which WebSocket implementation to use | ||
@@ -28,2 +26,6 @@ */ | ||
/** | ||
* Triggered when a WebSocket connection encounters an error | ||
*/ | ||
onError?: (evt?: Event) => void; | ||
/** | ||
* Triggered when a WebSocket connection is closed | ||
@@ -49,28 +51,77 @@ */ | ||
}; | ||
/** | ||
* Send ping messages to the server and kill the connection if no pong message is returned | ||
*/ | ||
keepAlive?: { | ||
/** | ||
* @default false | ||
*/ | ||
enabled: boolean; | ||
/** | ||
* Send a ping message every this many milliseconds | ||
* @default 5_000 | ||
*/ | ||
intervalMs?: number; | ||
/** | ||
* Close the WebSocket after this many milliseconds if the server does not respond | ||
* @default 1_000 | ||
*/ | ||
pongTimeoutMs?: number; | ||
}; | ||
} | ||
/** | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export declare function createWSClient(opts: WebSocketClientOptions): { | ||
close: () => void; | ||
request: (op: Operation, callbacks: WSCallbackObserver<AnyRouter, unknown>) => UnsubscribeFn; | ||
request: (opts: { | ||
op: Operation; | ||
callbacks: WSCallbackObserver<AnyRouter, unknown>; | ||
lastEventId: string | undefined; | ||
}) => UnsubscribeFn; | ||
readonly connection: ({ | ||
id: number; | ||
} & ({ | ||
state: 'open'; | ||
state: "open"; | ||
ws: WebSocket; | ||
} | { | ||
state: 'closed'; | ||
state: "closed"; | ||
ws: WebSocket; | ||
} | { | ||
state: 'connecting'; | ||
ws?: WebSocket | undefined; | ||
state: "connecting"; | ||
ws?: WebSocket; | ||
})) | null; | ||
/** | ||
* Reconnect to the WebSocket server | ||
*/ | ||
reconnect: (cause: Error | null) => void; | ||
connectionState: import("@trpc/server/observable").BehaviorSubject<TRPCConnectionState<TRPCClientError<AnyRouter>>>; | ||
}; | ||
/** | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export type TRPCWebSocketClient = ReturnType<typeof createWSClient>; | ||
export interface WebSocketLinkOptions { | ||
/** | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export type WebSocketLinkOptions<TRouter extends AnyRouter> = { | ||
client: TRPCWebSocketClient; | ||
} | ||
} & TransformerOptions<inferClientTypes<TRouter>>; | ||
/** | ||
* @see https://trpc.io/docs/client/links/wsLink | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export declare function wsLink<TRouter extends AnyRouter>(opts: WebSocketLinkOptions): TRPCLink<TRouter>; | ||
export declare function wsLink<TRouter extends AnyRouter>(opts: WebSocketLinkOptions<TRouter>): TRPCLink<TRouter>; | ||
export {}; | ||
//# sourceMappingURL=wsLink.d.ts.map |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var observable = require('@trpc/server/observable'); | ||
var unstableCoreDoNotImport = require('@trpc/server/unstable-core-do-not-import'); | ||
var TRPCClientError = require('../TRPCClientError.js'); | ||
var transformer = require('../internals/transformer.js'); | ||
var urlWithConnectionParams = require('./internals/urlWithConnectionParams.js'); | ||
var observable = require('@trpc/core/observable'); | ||
var transformResult = require('../transformResult-150436c9.js'); | ||
var TRPCClientError = require('../TRPCClientError-67aefe1c.js'); | ||
require('@trpc/core'); | ||
const run = (fn)=>fn(); | ||
@@ -16,4 +15,9 @@ const exponentialBackoff = (attemptIndex)=>attemptIndex === 0 ? 0 : Math.min(1000 * 2 ** attemptIndex, 30000); | ||
}; | ||
function createWSClient(opts) { | ||
const { url , WebSocket: WebSocketImpl = WebSocket , retryDelayMs: retryDelayFn = exponentialBackoff , onOpen , onClose , } = opts; | ||
/** | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ function createWSClient(opts) { | ||
const { WebSocket: WebSocketImpl = WebSocket, retryDelayMs: retryDelayFn = exponentialBackoff } = opts; | ||
const lazyOpts = { | ||
@@ -35,2 +39,12 @@ ...lazyDefaults, | ||
let activeConnection = lazyOpts.enabled ? null : createConnection(); | ||
const initState = activeConnection ? { | ||
type: 'state', | ||
state: 'connecting', | ||
error: null | ||
} : { | ||
type: 'state', | ||
state: 'idle', | ||
error: null | ||
}; | ||
const connectionState = observable.behaviorSubject(initState); | ||
/** | ||
@@ -40,3 +54,3 @@ * tries to send the list of messages | ||
if (!activeConnection) { | ||
activeConnection = createConnection(); | ||
reconnect(null); | ||
return; | ||
@@ -66,9 +80,8 @@ } | ||
} | ||
function tryReconnect(conn) { | ||
function tryReconnect(cause) { | ||
if (!!connectTimer) { | ||
return; | ||
} | ||
conn.state = 'connecting'; | ||
const timeout = retryDelayFn(connectAttempt++); | ||
reconnectInMs(timeout); | ||
reconnectInMs(timeout, cause); | ||
} | ||
@@ -82,5 +95,5 @@ function hasPendingRequests(conn) { | ||
} | ||
function reconnect() { | ||
function reconnect(cause) { | ||
if (lazyOpts.enabled && !hasPendingRequests()) { | ||
// Skip reconnecting if there are pending requests and we're in lazy mode | ||
// Skip reconnecting if there aren't pending requests and we're in lazy mode | ||
return; | ||
@@ -90,9 +103,21 @@ } | ||
activeConnection = createConnection(); | ||
oldConnection && closeIfNoPending(oldConnection); | ||
if (oldConnection) { | ||
closeIfNoPending(oldConnection); | ||
} | ||
const currentState = connectionState.get(); | ||
if (currentState.state !== 'connecting') { | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'connecting', | ||
error: cause ? TRPCClientError.TRPCClientError.from(cause) : null | ||
}); | ||
} | ||
} | ||
function reconnectInMs(ms) { | ||
function reconnectInMs(ms, cause) { | ||
if (connectTimer) { | ||
return; | ||
} | ||
connectTimer = setTimeout(reconnect, ms); | ||
connectTimer = setTimeout(()=>{ | ||
reconnect(cause); | ||
}, ms); | ||
} | ||
@@ -109,3 +134,7 @@ function closeIfNoPending(conn) { | ||
} | ||
request(req.op, req.callbacks); | ||
request({ | ||
op: req.op, | ||
callbacks: req.callbacks, | ||
lastEventId: req.lastEventId | ||
}); | ||
} | ||
@@ -121,5 +150,10 @@ const startLazyDisconnectTimer = ()=>{ | ||
} | ||
if (!hasPendingRequests(activeConnection)) { | ||
if (!hasPendingRequests()) { | ||
activeConnection.ws?.close(); | ||
activeConnection = null; | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'idle', | ||
error: null | ||
}); | ||
} | ||
@@ -129,2 +163,4 @@ }, lazyOpts.closeMs); | ||
function createConnection() { | ||
let pingTimeout = undefined; | ||
let pongTimeout = undefined; | ||
const self = { | ||
@@ -135,24 +171,119 @@ id: ++connectionIndex, | ||
clearTimeout(lazyDisconnectTimer); | ||
const onError = ()=>{ | ||
function destroy() { | ||
const noop = ()=>{ | ||
// no-op | ||
}; | ||
const { ws } = self; | ||
if (ws) { | ||
ws.onclose = noop; | ||
ws.onerror = noop; | ||
ws.onmessage = noop; | ||
ws.onopen = noop; | ||
ws.close(); | ||
} | ||
self.state = 'closed'; | ||
if (self === activeConnection) { | ||
tryReconnect(self); | ||
} | ||
const onCloseOrError = (cause)=>{ | ||
clearTimeout(pingTimeout); | ||
clearTimeout(pongTimeout); | ||
self.state = 'closed'; | ||
if (activeConnection === self) { | ||
// connection might have been replaced already | ||
tryReconnect(cause); | ||
} | ||
for (const [key, req] of Object.entries(pendingRequests)){ | ||
if (req.connection !== self) { | ||
continue; | ||
} | ||
// The connection was closed either unexpectedly or because of a reconnect | ||
if (req.type === 'subscription') { | ||
// Subscriptions will resume after we've reconnected | ||
resumeSubscriptionOnReconnect(req); | ||
} else { | ||
// Queries and mutations will error if interrupted | ||
delete pendingRequests[key]; | ||
req.callbacks.error?.(TRPCClientError.TRPCClientError.from(cause ?? new TRPCWebSocketClosedError())); | ||
} | ||
} | ||
}; | ||
run(async ()=>{ | ||
const urlString = typeof url === 'function' ? await url() : url; | ||
const ws = new WebSocketImpl(urlString); | ||
const onError = (evt)=>{ | ||
onCloseOrError(new TRPCWebSocketClosedError({ | ||
cause: evt | ||
})); | ||
opts.onError?.(evt); | ||
}; | ||
function connect(url) { | ||
if (opts.connectionParams) { | ||
// append `?connectionParams=1` when connection params are used | ||
const prefix = url.includes('?') ? '&' : '?'; | ||
url += prefix + 'connectionParams=1'; | ||
} | ||
const ws = new WebSocketImpl(url); | ||
self.ws = ws; | ||
clearTimeout(connectTimer); | ||
connectTimer = undefined; | ||
ws.addEventListener('open', ()=>{ | ||
/* istanbul ignore next -- @preserve */ if (activeConnection?.ws !== ws) { | ||
return; | ||
ws.onopen = ()=>{ | ||
async function sendConnectionParams() { | ||
if (!opts.connectionParams) { | ||
return; | ||
} | ||
const connectMsg = { | ||
method: 'connectionParams', | ||
data: await urlWithConnectionParams.resultOf(opts.connectionParams) | ||
}; | ||
ws.send(JSON.stringify(connectMsg)); | ||
} | ||
connectAttempt = 0; | ||
self.state = 'open'; | ||
onOpen?.(); | ||
dispatch(); | ||
}); | ||
ws.addEventListener('error', onError); | ||
function handleKeepAlive() { | ||
if (!opts.keepAlive?.enabled) { | ||
return; | ||
} | ||
const { pongTimeoutMs = 1000, intervalMs = 5000 } = opts.keepAlive; | ||
const schedulePing = ()=>{ | ||
const schedulePongTimeout = ()=>{ | ||
pongTimeout = setTimeout(()=>{ | ||
const wasOpen = self.state === 'open'; | ||
destroy(); | ||
if (wasOpen) { | ||
opts.onClose?.(); | ||
} | ||
}, pongTimeoutMs); | ||
}; | ||
pingTimeout = setTimeout(()=>{ | ||
ws.send('PING'); | ||
schedulePongTimeout(); | ||
}, intervalMs); | ||
}; | ||
ws.addEventListener('message', ()=>{ | ||
clearTimeout(pingTimeout); | ||
clearTimeout(pongTimeout); | ||
schedulePing(); | ||
}); | ||
schedulePing(); | ||
} | ||
run(async ()=>{ | ||
/* istanbul ignore next -- @preserve */ if (activeConnection?.ws !== ws) { | ||
return; | ||
} | ||
handleKeepAlive(); | ||
await sendConnectionParams(); | ||
connectAttempt = 0; | ||
self.state = 'open'; | ||
// Update connection state | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'pending', | ||
error: null | ||
}); | ||
opts.onOpen?.(); | ||
dispatch(); | ||
}).catch((cause)=>{ | ||
ws.close(// "Status codes in the range 3000-3999 are reserved for use by libraries, frameworks, and applications" | ||
3000); | ||
onCloseOrError(new TRPCWebSocketClosedError({ | ||
message: 'Initialization error', | ||
cause | ||
})); | ||
}); | ||
}; | ||
ws.onerror = onError; | ||
const handleIncomingRequest = (req)=>{ | ||
@@ -163,3 +294,5 @@ if (self !== activeConnection) { | ||
if (req.method === 'reconnect') { | ||
reconnect(); | ||
reconnect(new TRPCWebSocketClosedError({ | ||
message: 'Server requested reconnect' | ||
})); | ||
// notify subscribers | ||
@@ -181,7 +314,12 @@ for (const pendingReq of Object.values(pendingRequests)){ | ||
if (self === activeConnection && req.connection !== activeConnection) { | ||
// gracefully replace old connection with this | ||
const oldConn = req.connection; | ||
// gracefully replace old connection with a new connection | ||
req.connection = self; | ||
oldConn && closeIfNoPending(oldConn); | ||
} | ||
if (req.connection !== self) { | ||
// the connection has been replaced | ||
return; | ||
} | ||
if ('result' in data && data.result.type === 'data' && typeof data.result.id === 'string') { | ||
req.lastEventId = data.result.id; | ||
} | ||
if ('result' in data && data.result.type === 'stopped' && activeConnection === self) { | ||
@@ -191,3 +329,11 @@ req.callbacks.complete(); | ||
}; | ||
ws.addEventListener('message', ({ data })=>{ | ||
ws.onmessage = (event)=>{ | ||
const { data } = event; | ||
if (data === 'PONG') { | ||
return; | ||
} | ||
if (data === 'PING') { | ||
ws.send('PONG'); | ||
return; | ||
} | ||
startLazyDisconnectTimer(); | ||
@@ -204,40 +350,22 @@ const msg = JSON.parse(data); | ||
} | ||
}); | ||
ws.addEventListener('close', ({ code })=>{ | ||
if (self.state === 'open') { | ||
onClose?.({ | ||
code | ||
}); | ||
}; | ||
ws.onclose = (event)=>{ | ||
const wasOpen = self.state === 'open'; | ||
destroy(); | ||
onCloseOrError(new TRPCWebSocketClosedError({ | ||
cause: event | ||
})); | ||
if (wasOpen) { | ||
opts.onClose?.(event); | ||
} | ||
self.state = 'closed'; | ||
if (activeConnection === self) { | ||
// connection might have been replaced already | ||
tryReconnect(self); | ||
} | ||
for (const [key, req] of Object.entries(pendingRequests)){ | ||
if (req.connection !== self) { | ||
continue; | ||
} | ||
if (self.state === 'closed') { | ||
// If the connection was closed, we just call `complete()` on the request | ||
delete pendingRequests[key]; | ||
req.callbacks.complete?.(); | ||
continue; | ||
} | ||
// The connection was closed either unexpectedly or because of a reconnect | ||
if (req.type === 'subscription') { | ||
// Subscriptions will resume after we've reconnected | ||
resumeSubscriptionOnReconnect(req); | ||
} else { | ||
// Queries and mutations will error if interrupted | ||
delete pendingRequests[key]; | ||
req.callbacks.error?.(TRPCClientError.TRPCClientError.from(new TRPCWebSocketClosedError('WebSocket closed prematurely'))); | ||
} | ||
} | ||
}); | ||
}).catch(onError); | ||
}; | ||
} | ||
Promise.resolve(urlWithConnectionParams.resultOf(opts.url)).then(connect).catch(()=>{ | ||
onCloseOrError(new Error('Failed to resolve url')); | ||
}); | ||
return self; | ||
} | ||
function request(op, callbacks) { | ||
const { type , input , path , id } = op; | ||
function request(opts) { | ||
const { op, callbacks, lastEventId } = opts; | ||
const { type, input, path, id } = op; | ||
const envelope = { | ||
@@ -248,3 +376,4 @@ id, | ||
input, | ||
path | ||
path, | ||
lastEventId | ||
} | ||
@@ -256,3 +385,4 @@ }; | ||
callbacks, | ||
op | ||
op, | ||
lastEventId | ||
}; | ||
@@ -285,6 +415,10 @@ // enqueue message | ||
// close pending requests that aren't attached to a connection yet | ||
req.callbacks.error(TRPCClientError.TRPCClientError.from(new Error('Closed before connection was established'))); | ||
req.callbacks.error(TRPCClientError.TRPCClientError.from(new TRPCWebSocketClosedError({ | ||
message: 'Closed before connection was established' | ||
}))); | ||
} | ||
} | ||
activeConnection && closeIfNoPending(activeConnection); | ||
if (activeConnection) { | ||
closeIfNoPending(activeConnection); | ||
} | ||
clearTimeout(connectTimer); | ||
@@ -297,8 +431,16 @@ connectTimer = undefined; | ||
return activeConnection; | ||
} | ||
}, | ||
/** | ||
* Reconnect to the WebSocket server | ||
*/ reconnect, | ||
connectionState: connectionState | ||
}; | ||
} | ||
class TRPCWebSocketClosedError extends Error { | ||
constructor(message){ | ||
super(message); | ||
constructor(opts){ | ||
super(opts?.message ?? 'WebSocket closed', // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore https://github.com/tc39/proposal-error-cause | ||
{ | ||
cause: opts?.cause | ||
}); | ||
this.name = 'TRPCWebSocketClosedError'; | ||
@@ -309,42 +451,60 @@ Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype); | ||
/** | ||
* @see https://trpc.io/docs/client/links/wsLink | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ function wsLink(opts) { | ||
return (runtime)=>{ | ||
const { client } = opts; | ||
return ({ op })=>{ | ||
const transformer$1 = transformer.getTransformer(opts.transformer); | ||
return ()=>{ | ||
const { client } = opts; | ||
return ({ op })=>{ | ||
return observable.observable((observer)=>{ | ||
const { type , path , id , context } = op; | ||
const input = runtime.transformer.serialize(op.input); | ||
const unsub = client.request({ | ||
type, | ||
path, | ||
input, | ||
id, | ||
context | ||
}, { | ||
error (err) { | ||
observer.error(err); | ||
unsub(); | ||
}, | ||
complete () { | ||
observer.complete(); | ||
}, | ||
next (message) { | ||
const transformed = transformResult.transformResult(message, runtime); | ||
if (!transformed.ok) { | ||
observer.error(TRPCClientError.TRPCClientError.from(transformed.error)); | ||
return; | ||
} | ||
const { type, path, id, context } = op; | ||
const input = transformer$1.input.serialize(op.input); | ||
const connState = type === 'subscription' ? client.connectionState.subscribe({ | ||
next (result) { | ||
observer.next({ | ||
result: transformed.result | ||
result, | ||
context | ||
}); | ||
if (op.type !== 'subscription') { | ||
// if it isn't a subscription we don't care about next response | ||
unsub(); | ||
} | ||
}) : null; | ||
const unsubscribeRequest = client.request({ | ||
op: { | ||
type, | ||
path, | ||
input, | ||
id, | ||
context, | ||
signal: null | ||
}, | ||
callbacks: { | ||
error (err) { | ||
observer.error(err); | ||
unsubscribeRequest(); | ||
}, | ||
complete () { | ||
observer.complete(); | ||
}, | ||
next (event) { | ||
const transformed = unstableCoreDoNotImport.transformResult(event, transformer$1.output); | ||
if (!transformed.ok) { | ||
observer.error(TRPCClientError.TRPCClientError.from(transformed.error)); | ||
return; | ||
} | ||
observer.next({ | ||
result: transformed.result | ||
}); | ||
if (op.type !== 'subscription') { | ||
// if it isn't a subscription we don't care about next response | ||
unsubscribeRequest(); | ||
observer.complete(); | ||
} | ||
} | ||
} | ||
}, | ||
lastEventId: undefined | ||
}); | ||
return ()=>{ | ||
unsub(); | ||
unsubscribeRequest(); | ||
connState?.unsubscribe(); | ||
}; | ||
@@ -351,0 +511,0 @@ }); |
@@ -1,3 +0,4 @@ | ||
import { DefaultErrorShape, inferErrorShape, Maybe, TRPCInferrable } from '@trpc/core'; | ||
import { TRPCErrorResponse } from '@trpc/core/rpc'; | ||
import type { inferClientTypes, InferrableClientTypes, Maybe, TRPCErrorResponse } from '@trpc/server/unstable-core-do-not-import'; | ||
import { type DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import'; | ||
type inferErrorShape<TInferrable extends InferrableClientTypes> = inferClientTypes<TInferrable>['errorShape']; | ||
export interface TRPCClientErrorBase<TShape extends DefaultErrorShape> { | ||
@@ -8,4 +9,4 @@ readonly message: string; | ||
} | ||
export type TRPCClientErrorLike<TInferrable extends TRPCInferrable> = TRPCClientErrorBase<inferErrorShape<TInferrable>>; | ||
export declare class TRPCClientError<TRouterOrProcedure extends TRPCInferrable> extends Error implements TRPCClientErrorBase<inferErrorShape<TRouterOrProcedure>> { | ||
export type TRPCClientErrorLike<TInferrable extends InferrableClientTypes> = TRPCClientErrorBase<inferErrorShape<TInferrable>>; | ||
export declare class TRPCClientError<TRouterOrProcedure extends InferrableClientTypes> extends Error implements TRPCClientErrorBase<inferErrorShape<TRouterOrProcedure>> { | ||
readonly cause: Error | undefined; | ||
@@ -24,6 +25,7 @@ readonly shape: Maybe<inferErrorShape<TRouterOrProcedure>>; | ||
}); | ||
static from<TRouterOrProcedure extends TRPCInferrable>(_cause: Error | TRPCErrorResponse<any>, opts?: { | ||
static from<TRouterOrProcedure extends InferrableClientTypes>(_cause: Error | TRPCErrorResponse<any> | object, opts?: { | ||
meta?: Record<string, unknown>; | ||
}): TRPCClientError<TRouterOrProcedure>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=TRPCClientError.d.ts.map |
{ | ||
"name": "@trpc/client", | ||
"version": "11.0.0-alpha-tmp.234+517856c20", | ||
"version": "11.0.0-alpha-tmp-01-21-trpc-monorepo-prebuilt.739+b648648f8", | ||
"description": "The tRPC client library", | ||
@@ -28,3 +28,3 @@ "author": "KATT", | ||
"codegen-entrypoints": "tsx entrypoints.script.ts", | ||
"lint": "eslint --cache --ext \".js,.ts,.tsx\" --ignore-path ../../.gitignore --report-unused-disable-directives src", | ||
"lint": "eslint --cache src", | ||
"ts-watch": "tsc --watch" | ||
@@ -64,6 +64,6 @@ }, | ||
}, | ||
"./shared": { | ||
"import": "./dist/shared/index.mjs", | ||
"require": "./dist/shared/index.js", | ||
"default": "./dist/shared/index.js" | ||
"./unstable-internals": { | ||
"import": "./dist/unstable-internals.mjs", | ||
"require": "./dist/unstable-internals.js", | ||
"default": "./dist/unstable-internals.js" | ||
} | ||
@@ -77,19 +77,22 @@ }, | ||
"links", | ||
"shared", | ||
"!**/*.test.*" | ||
"unstable-internals", | ||
"!**/*.test.*", | ||
"!**/__tests__" | ||
], | ||
"dependencies": { | ||
"@trpc/core": "11.0.0-alpha-tmp.234+517856c20" | ||
"peerDependencies": { | ||
"@trpc/server": "11.0.0-alpha-tmp-01-21-trpc-monorepo-prebuilt.739+b648648f8", | ||
"typescript": ">=5.7.2" | ||
}, | ||
"devDependencies": { | ||
"@testing-library/dom": "^9.0.0", | ||
"@trpc/server": "11.0.0-alpha-tmp-01-21-trpc-monorepo-prebuilt.739+b648648f8", | ||
"@types/isomorphic-fetch": "^0.0.39", | ||
"@types/node": "^20.10.0", | ||
"eslint": "^8.40.0", | ||
"@types/node": "^22.9.0", | ||
"eslint": "^9.13.0", | ||
"isomorphic-fetch": "^3.0.0", | ||
"node-fetch": "^3.3.0", | ||
"rollup": "^2.79.1", | ||
"rollup": "^4.24.4", | ||
"tslib": "^2.8.1", | ||
"tsx": "^4.0.0", | ||
"undici": "^6.0.1", | ||
"vitest": "^0.32.0" | ||
"typescript": "^5.7.2", | ||
"undici": "^7.0.0" | ||
}, | ||
@@ -102,3 +105,3 @@ "publishConfig": { | ||
], | ||
"gitHead": "517856c209e3647bbb42460b90ec2c54e334dc1c" | ||
"gitHead": "b648648f888f5f458d32402ef006787616c1b080" | ||
} |
@@ -27,12 +27,12 @@ <p align="center"> | ||
# npm | ||
npm install @trpc/client | ||
npm install @trpc/client@next | ||
# Yarn | ||
yarn add @trpc/client | ||
yarn add @trpc/client@next | ||
# pnpm | ||
pnpm add @trpc/client | ||
pnpm add @trpc/client@next | ||
# Bun | ||
bun add @trpc/client | ||
bun add @trpc/client@next | ||
``` | ||
@@ -39,0 +39,0 @@ |
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
import type { Unsubscribable } from '@trpc/server/observable'; | ||
import type { | ||
AnyMutationProcedure, | ||
AnyProcedure, | ||
AnyQueryProcedure, | ||
AnyRootConfig, | ||
AnyRouter, | ||
AnySubscriptionProcedure, | ||
ProcedureArgs, | ||
ProcedureRouterRecord, | ||
coerceToRouterRecord, | ||
inferClientTypes, | ||
inferProcedureInput, | ||
inferTransformedProcedureOutput, | ||
IntersectionError, | ||
ProcedureOptions, | ||
ProcedureType, | ||
} from '@trpc/core'; | ||
RouterRecord, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { | ||
createFlatProxy, | ||
createRecursiveProxy, | ||
inferTransformedProcedureOutput, | ||
inferTransformedSubscriptionOutput, | ||
IntersectionError, | ||
} from '@trpc/core'; | ||
import type { Unsubscribable } from '@trpc/core/observable'; | ||
import { CreateTRPCClientOptions } from './createTRPCUntypedClient'; | ||
import { | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import type { CreateTRPCClientOptions } from './createTRPCUntypedClient'; | ||
import type { | ||
TRPCSubscriptionObserver, | ||
TRPCUntypedClient, | ||
UntypedClientProperties, | ||
} from './internals/TRPCUntypedClient'; | ||
import { TRPCClientError } from './TRPCClientError'; | ||
import { TRPCUntypedClient } from './internals/TRPCUntypedClient'; | ||
import type { TRPCClientError } from './TRPCClientError'; | ||
@@ -35,42 +33,44 @@ /** | ||
type ResolverDef = { | ||
input: any; | ||
output: any; | ||
transformer: boolean; | ||
errorShape: any; | ||
}; | ||
type coerceAsyncGeneratorToIterable<T> = | ||
T extends AsyncGenerator<infer $T, infer $Return, infer $Next> | ||
? AsyncIterable<$T, $Return, $Next> | ||
: T; | ||
/** @internal */ | ||
export type Resolver< | ||
TConfig extends AnyRootConfig, | ||
TProcedure extends AnyProcedure, | ||
> = ( | ||
...args: ProcedureArgs<TProcedure['_def']> | ||
) => Promise<inferTransformedProcedureOutput<TConfig, TProcedure>>; | ||
export type Resolver<TDef extends ResolverDef> = ( | ||
input: TDef['input'], | ||
opts?: ProcedureOptions, | ||
) => Promise<coerceAsyncGeneratorToIterable<TDef['output']>>; | ||
type SubscriptionResolver< | ||
TConfig extends AnyRootConfig, | ||
TProcedure extends AnyProcedure, | ||
> = ( | ||
...args: [ | ||
input: ProcedureArgs<TProcedure['_def']>[0], | ||
opts: Partial< | ||
TRPCSubscriptionObserver< | ||
inferTransformedSubscriptionOutput<TConfig, TProcedure>, | ||
TRPCClientError<TConfig> | ||
> | ||
> & | ||
ProcedureArgs<TProcedure['_def']>[1], | ||
] | ||
type SubscriptionResolver<TDef extends ResolverDef> = ( | ||
input: TDef['input'], | ||
opts: Partial< | ||
TRPCSubscriptionObserver<TDef['output'], TRPCClientError<TDef>> | ||
> & | ||
ProcedureOptions, | ||
) => Unsubscribable; | ||
type DecorateProcedure< | ||
TConfig extends AnyRootConfig, | ||
TProcedure extends AnyProcedure, | ||
> = TProcedure extends AnyQueryProcedure | ||
TType extends ProcedureType, | ||
TDef extends ResolverDef, | ||
> = TType extends 'query' | ||
? { | ||
query: Resolver<TConfig, TProcedure>; | ||
query: Resolver<TDef>; | ||
} | ||
: TProcedure extends AnyMutationProcedure | ||
? { | ||
mutate: Resolver<TConfig, TProcedure>; | ||
} | ||
: TProcedure extends AnySubscriptionProcedure | ||
? { | ||
subscribe: SubscriptionResolver<TConfig, TProcedure>; | ||
} | ||
: never; | ||
: TType extends 'mutation' | ||
? { | ||
mutate: Resolver<TDef>; | ||
} | ||
: TType extends 'subscription' | ||
? { | ||
subscribe: SubscriptionResolver<TDef>; | ||
} | ||
: never; | ||
@@ -82,8 +82,21 @@ /** | ||
TRouter extends AnyRouter, | ||
TProcedures extends ProcedureRouterRecord, | ||
TRecord extends RouterRecord, | ||
> = { | ||
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter | ||
? DecoratedProcedureRecord<TRouter, TProcedures[TKey]['_def']['record']> | ||
: TProcedures[TKey] extends AnyProcedure | ||
? DecorateProcedure<TRouter['_def']['_config'], TProcedures[TKey]> | ||
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value | ||
? $Value extends AnyProcedure | ||
? DecorateProcedure< | ||
$Value['_def']['type'], | ||
{ | ||
input: inferProcedureInput<$Value>; | ||
output: inferTransformedProcedureOutput< | ||
inferClientTypes<TRouter>, | ||
$Value | ||
>; | ||
errorShape: inferClientTypes<TRouter>['errorShape']; | ||
transformer: inferClientTypes<TRouter>['transformer']; | ||
} | ||
> | ||
: $Value extends RouterRecord | AnyRouter | ||
? DecoratedProcedureRecord<TRouter, coerceToRouterRecord<$Value>> | ||
: never | ||
: never; | ||
@@ -112,6 +125,6 @@ }; | ||
export type CreateTRPCClient<TRouter extends AnyRouter> = | ||
inferRouterClient<TRouter> extends infer $ProcedureRecord | ||
? UntypedClientProperties & keyof $ProcedureRecord extends never | ||
inferRouterClient<TRouter> extends infer $Value | ||
? UntypedClientProperties & keyof $Value extends never | ||
? inferRouterClient<TRouter> | ||
: IntersectionError<UntypedClientProperties & keyof $ProcedureRecord> | ||
: IntersectionError<UntypedClientProperties & keyof $Value> | ||
: never; | ||
@@ -125,2 +138,12 @@ | ||
): CreateTRPCClient<TRouter> { | ||
const proxy = createRecursiveProxy<CreateTRPCClient<TRouter>>( | ||
({ path, args }) => { | ||
const pathCopy = [...path]; | ||
const procedureType = clientCallTypeToProcedureType(pathCopy.pop()!); | ||
const fullPath = pathCopy.join('.'); | ||
return (client as any)[procedureType](fullPath, ...args); | ||
}, | ||
); | ||
return createFlatProxy<CreateTRPCClient<TRouter>>((key) => { | ||
@@ -133,10 +156,3 @@ if (client.hasOwnProperty(key)) { | ||
} | ||
return createRecursiveProxy(({ path, args }) => { | ||
const pathCopy = [key, ...path]; | ||
const procedureType = clientCallTypeToProcedureType(pathCopy.pop()!); | ||
const fullPath = pathCopy.join('.'); | ||
return (client as any)[procedureType](fullPath, ...args); | ||
}); | ||
return proxy[key]; | ||
}); | ||
@@ -143,0 +159,0 @@ } |
@@ -1,6 +0,4 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { | ||
CreateTRPCClientOptions, | ||
TRPCUntypedClient, | ||
} from './internals/TRPCUntypedClient'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { CreateTRPCClientOptions } from './internals/TRPCUntypedClient'; | ||
import { TRPCUntypedClient } from './internals/TRPCUntypedClient'; | ||
@@ -7,0 +5,0 @@ export function createTRPCUntypedClient<TRouter extends AnyRouter>( |
@@ -1,2 +0,2 @@ | ||
import { FetchEsque, NativeFetchEsque } from './internals/types'; | ||
import type { FetchEsque, NativeFetchEsque } from './internals/types'; | ||
@@ -3,0 +3,0 @@ type AnyFn = (...args: any[]) => unknown; |
@@ -0,1 +1,3 @@ | ||
// TODO: Be explicit about what we export here | ||
export * from './createTRPCUntypedClient'; | ||
@@ -2,0 +4,0 @@ export * from './createTRPCClient'; |
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
import { CancelFn, PromiseAndCancel } from '../links/types'; | ||
@@ -13,13 +12,6 @@ type BatchItem<TKey, TValue> = { | ||
items: BatchItem<TKey, TValue>[]; | ||
cancel: CancelFn; | ||
}; | ||
type BatchLoader<TKey, TValue> = { | ||
export type BatchLoader<TKey, TValue> = { | ||
validate: (keys: TKey[]) => boolean; | ||
fetch: ( | ||
keys: TKey[], | ||
unitResolver: (index: number, value: NonNullable<TValue>) => void, | ||
) => { | ||
promise: Promise<TValue[]>; | ||
cancel: CancelFn; | ||
}; | ||
fetch: (keys: TKey[]) => Promise<TValue[] | Promise<TValue>[]>; | ||
}; | ||
@@ -106,3 +98,2 @@ | ||
items, | ||
cancel: throwFatalError, | ||
}; | ||
@@ -112,21 +103,23 @@ for (const item of items) { | ||
} | ||
const unitResolver = (index: number, value: NonNullable<TValue>) => { | ||
const item = batch.items[index]!; | ||
item.resolve?.(value); | ||
item.batch = null; | ||
item.reject = null; | ||
item.resolve = null; | ||
}; | ||
const { promise, cancel } = batchLoader.fetch( | ||
batch.items.map((_item) => _item.key), | ||
unitResolver, | ||
); | ||
batch.cancel = cancel; | ||
const promise = batchLoader.fetch(batch.items.map((_item) => _item.key)); | ||
promise | ||
.then((result) => { | ||
for (let i = 0; i < result.length; i++) { | ||
const value = result[i]!; | ||
unitResolver(i, value); | ||
} | ||
.then(async (result) => { | ||
await Promise.all( | ||
result.map(async (valueOrPromise, index) => { | ||
const item = batch.items[index]!; | ||
try { | ||
const value = await Promise.resolve(valueOrPromise); | ||
item.resolve?.(value); | ||
} catch (cause) { | ||
item.reject?.(cause as Error); | ||
} | ||
item.batch = null; | ||
item.reject = null; | ||
item.resolve = null; | ||
}), | ||
); | ||
for (const item of batch.items) { | ||
@@ -145,3 +138,3 @@ item.reject?.(new Error('Missing result')); | ||
} | ||
function load(key: TKey): PromiseAndCancel<TValue> { | ||
function load(key: TKey): Promise<TValue> { | ||
const item: BatchItem<TKey, TValue> = { | ||
@@ -168,13 +161,4 @@ aborted: false, | ||
} | ||
const cancel = () => { | ||
item.aborted = true; | ||
if (item.batch?.items.every((item) => item.aborted)) { | ||
// All items in the batch have been cancelled | ||
item.batch.cancel(); | ||
item.batch = null; | ||
} | ||
}; | ||
return { promise, cancel }; | ||
return promise; | ||
} | ||
@@ -181,0 +165,0 @@ |
@@ -1,15 +0,16 @@ | ||
import { | ||
AnyRouter, | ||
CombinedDataTransformer, | ||
DataTransformerOptions, | ||
DefaultDataTransformer, | ||
} from '@trpc/core'; | ||
import { | ||
import type { | ||
inferObservableValue, | ||
observableToPromise, | ||
share, | ||
Unsubscribable, | ||
} from '@trpc/core/observable'; | ||
} from '@trpc/server/observable'; | ||
import { observableToPromise, share } from '@trpc/server/observable'; | ||
import type { | ||
AnyRouter, | ||
inferAsyncIterableYield, | ||
InferrableClientTypes, | ||
Maybe, | ||
TypeError, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { createChain } from '../links/internals/createChain'; | ||
import { | ||
import type { TRPCConnectionState } from '../links/internals/subscriptions'; | ||
import type { | ||
OperationContext, | ||
@@ -22,35 +23,2 @@ OperationLink, | ||
type CreateTRPCClientBaseOptions<TRouter extends AnyRouter> = | ||
TRouter['_def']['_config']['transformer'] extends DefaultDataTransformer | ||
? { | ||
/** | ||
* Data transformer | ||
* | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/data-transformers | ||
**/ | ||
transformer?: 'You must set a transformer on the backend router'; | ||
} | ||
: TRouter['_def']['_config']['transformer'] extends DataTransformerOptions | ||
? { | ||
/** | ||
* Data transformer | ||
* | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/data-transformers | ||
**/ | ||
transformer: TRouter['_def']['_config']['transformer'] extends CombinedDataTransformer | ||
? DataTransformerOptions | ||
: TRouter['_def']['_config']['transformer']; | ||
} | ||
: { | ||
/** | ||
* Data transformer | ||
* | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/data-transformers | ||
**/ | ||
transformer?: CombinedDataTransformer; | ||
}; | ||
type TRPCType = 'mutation' | 'query' | 'subscription'; | ||
@@ -66,14 +34,15 @@ export interface TRPCRequestOptions { | ||
export interface TRPCSubscriptionObserver<TValue, TError> { | ||
onStarted: () => void; | ||
onData: (value: TValue) => void; | ||
onStarted: (opts: { context: OperationContext | undefined }) => void; | ||
onData: (value: inferAsyncIterableYield<TValue>) => void; | ||
onError: (err: TError) => void; | ||
onStopped: () => void; | ||
onComplete: () => void; | ||
onConnectionStateChange: (state: TRPCConnectionState<TError>) => void; | ||
} | ||
/** @internal */ | ||
export type CreateTRPCClientOptions<TRouter extends AnyRouter> = | ||
| CreateTRPCClientBaseOptions<TRouter> & { | ||
links: TRPCLink<TRouter>[]; | ||
}; | ||
export type CreateTRPCClientOptions<TRouter extends InferrableClientTypes> = { | ||
links: TRPCLink<TRouter>[]; | ||
transformer?: TypeError<'The transformer property has moved to httpLink/httpBatchLink/wsLink'>; | ||
}; | ||
@@ -99,36 +68,4 @@ /** @internal */ | ||
const combinedTransformer: CombinedDataTransformer = (() => { | ||
const transformer = opts.transformer as | ||
| DataTransformerOptions | ||
| undefined; | ||
this.runtime = {}; | ||
if (!transformer) { | ||
return { | ||
input: { | ||
serialize: (data) => data, | ||
deserialize: (data) => data, | ||
}, | ||
output: { | ||
serialize: (data) => data, | ||
deserialize: (data) => data, | ||
}, | ||
}; | ||
} | ||
if ('input' in transformer) { | ||
return opts.transformer as CombinedDataTransformer; | ||
} | ||
return { | ||
input: transformer, | ||
output: transformer, | ||
}; | ||
})(); | ||
this.runtime = { | ||
transformer: { | ||
serialize: (data) => combinedTransformer.input.serialize(data), | ||
deserialize: (data) => combinedTransformer.output.deserialize(data), | ||
}, | ||
combinedTransformer, | ||
}; | ||
// Initialize the links | ||
@@ -138,8 +75,3 @@ this.links = opts.links.map((link) => link(this.runtime)); | ||
private $request<TInput = unknown, TOutput = unknown>({ | ||
type, | ||
input, | ||
path, | ||
context = {}, | ||
}: { | ||
private $request<TInput = unknown, TOutput = unknown>(opts: { | ||
type: TRPCType; | ||
@@ -149,2 +81,3 @@ input: TInput; | ||
context?: OperationContext; | ||
signal: Maybe<AbortSignal>; | ||
}) { | ||
@@ -154,7 +87,5 @@ const chain$ = createChain<AnyRouter, TInput, TOutput>({ | ||
op: { | ||
...opts, | ||
context: opts.context ?? {}, | ||
id: ++this.requestId, | ||
type, | ||
path, | ||
input, | ||
context, | ||
}, | ||
@@ -164,3 +95,4 @@ }); | ||
} | ||
private requestAsPromise<TInput = unknown, TOutput = unknown>(opts: { | ||
private async requestAsPromise<TInput = unknown, TOutput = unknown>(opts: { | ||
type: TRPCType; | ||
@@ -170,21 +102,14 @@ input: TInput; | ||
context?: OperationContext; | ||
signal?: AbortSignal; | ||
signal: Maybe<AbortSignal>; | ||
}): Promise<TOutput> { | ||
const req$ = this.$request<TInput, TOutput>(opts); | ||
type TValue = inferObservableValue<typeof req$>; | ||
const { promise, abort } = observableToPromise<TValue>(req$); | ||
try { | ||
const req$ = this.$request<TInput, TOutput>(opts); | ||
type TValue = inferObservableValue<typeof req$>; | ||
const abortablePromise = new Promise<TOutput>((resolve, reject) => { | ||
opts.signal?.addEventListener('abort', abort); | ||
promise | ||
.then((envelope) => { | ||
resolve((envelope.result as any).data); | ||
}) | ||
.catch((err) => { | ||
reject(TRPCClientError.from(err)); | ||
}); | ||
}); | ||
return abortablePromise; | ||
const envelope = await observableToPromise<TValue>(req$); | ||
const data = (envelope.result as any).data; | ||
return data; | ||
} catch (err) { | ||
throw TRPCClientError.from(err as Error); | ||
} | ||
} | ||
@@ -221,12 +146,27 @@ public query(path: string, input?: unknown, opts?: TRPCRequestOptions) { | ||
input, | ||
context: opts?.context, | ||
context: opts.context, | ||
signal: opts.signal, | ||
}); | ||
return observable$.subscribe({ | ||
next(envelope) { | ||
if (envelope.result.type === 'started') { | ||
opts.onStarted?.(); | ||
} else if (envelope.result.type === 'stopped') { | ||
opts.onStopped?.(); | ||
} else { | ||
opts.onData?.((envelope.result as any).data); | ||
switch (envelope.result.type) { | ||
case 'state': { | ||
opts.onConnectionStateChange?.(envelope.result); | ||
break; | ||
} | ||
case 'started': { | ||
opts.onStarted?.({ | ||
context: envelope.context, | ||
}); | ||
break; | ||
} | ||
case 'stopped': { | ||
opts.onStopped?.(); | ||
break; | ||
} | ||
case 'data': | ||
case undefined: { | ||
opts.onData?.(envelope.result.data); | ||
break; | ||
} | ||
} | ||
@@ -233,0 +173,0 @@ }, |
@@ -1,20 +0,2 @@ | ||
export type AbortControllerEsque = new () => AbortControllerInstanceEsque; | ||
/** | ||
* Allows you to abort one or more requests. | ||
*/ | ||
export interface AbortControllerInstanceEsque { | ||
/** | ||
* The AbortSignal object associated with this object. | ||
*/ | ||
readonly signal: AbortSignal; | ||
/** | ||
* Sets this object's AbortSignal's aborted flag and signals to | ||
* any observers that the associated activity is to be aborted. | ||
*/ | ||
abort(): void; | ||
} | ||
/** | ||
* A subset of the standard fetch function type needed by tRPC internally. | ||
@@ -55,3 +37,3 @@ * @see fetch from lib.dom.d.ts | ||
*/ | ||
body?: FormData | ReadableStream | string | null; | ||
body?: FormData | string | null | Uint8Array | Blob | File; | ||
@@ -71,3 +53,3 @@ /** | ||
*/ | ||
signal?: AbortSignal | null; | ||
signal?: AbortSignal | undefined; | ||
} | ||
@@ -83,2 +65,9 @@ | ||
export type NodeJSReadableStreamEsque = { | ||
on( | ||
eventName: string | symbol, | ||
listener: (...args: any[]) => void, | ||
): NodeJSReadableStreamEsque; | ||
}; | ||
/** | ||
@@ -89,3 +78,3 @@ * A subset of the standard Response properties needed by tRPC internally. | ||
export interface ResponseEsque { | ||
readonly body?: NodeJS.ReadableStream | WebReadableStreamEsque | null; | ||
readonly body?: NodeJSReadableStreamEsque | WebReadableStreamEsque | null; | ||
/** | ||
@@ -92,0 +81,0 @@ * @remarks |
@@ -1,50 +0,137 @@ | ||
import { NonEmptyArray } from '../internals/types'; | ||
import { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import type { AnyRouter, ProcedureType } from '@trpc/server'; | ||
import { observable } from '@trpc/server/observable'; | ||
import { transformResult } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { BatchLoader } from '../internals/dataLoader'; | ||
import { dataLoader } from '../internals/dataLoader'; | ||
import { allAbortSignals } from '../internals/signals'; | ||
import type { NonEmptyArray } from '../internals/types'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import type { HTTPResult } from './internals/httpUtils'; | ||
import { | ||
createHTTPBatchLink, | ||
RequesterFn, | ||
} from './internals/createHTTPBatchLink'; | ||
import { jsonHttpRequester } from './internals/httpUtils'; | ||
import { Operation } from './types'; | ||
getUrl, | ||
jsonHttpRequester, | ||
resolveHTTPLinkOptions, | ||
} from './internals/httpUtils'; | ||
import type { Operation, TRPCLink } from './types'; | ||
const batchRequester: RequesterFn<HTTPBatchLinkOptions> = (requesterOpts) => { | ||
return (batchOps) => { | ||
const path = batchOps.map((op) => op.path).join(','); | ||
const inputs = batchOps.map((op) => op.input); | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpBatchLink | ||
*/ | ||
export function httpBatchLink<TRouter extends AnyRouter>( | ||
opts: HTTPBatchLinkOptions<TRouter['_def']['_config']['$types']>, | ||
): TRPCLink<TRouter> { | ||
const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
const maxURLLength = opts.maxURLLength ?? Infinity; | ||
const { promise, cancel } = jsonHttpRequester({ | ||
...requesterOpts, | ||
path, | ||
inputs, | ||
headers() { | ||
if (!requesterOpts.opts.headers) { | ||
return {}; | ||
} | ||
if (typeof requesterOpts.opts.headers === 'function') { | ||
return requesterOpts.opts.headers({ | ||
opList: batchOps as NonEmptyArray<Operation>, | ||
return () => { | ||
const batchLoader = ( | ||
type: ProcedureType, | ||
): BatchLoader<Operation, HTTPResult> => { | ||
return { | ||
validate(batchOps) { | ||
if (maxURLLength === Infinity) { | ||
// escape hatch for quick calcs | ||
return true; | ||
} | ||
const path = batchOps.map((op) => op.path).join(','); | ||
const inputs = batchOps.map((op) => op.input); | ||
const url = getUrl({ | ||
...resolvedOpts, | ||
type, | ||
path, | ||
inputs, | ||
signal: null, | ||
}); | ||
return url.length <= maxURLLength; | ||
}, | ||
async fetch(batchOps) { | ||
const path = batchOps.map((op) => op.path).join(','); | ||
const inputs = batchOps.map((op) => op.input); | ||
const signal = allAbortSignals(...batchOps.map((op) => op.signal)); | ||
const res = await jsonHttpRequester({ | ||
...resolvedOpts, | ||
path, | ||
inputs, | ||
type, | ||
headers() { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
opList: batchOps as NonEmptyArray<Operation>, | ||
}); | ||
} | ||
return opts.headers; | ||
}, | ||
signal, | ||
}); | ||
const resJSON = Array.isArray(res.json) | ||
? res.json | ||
: batchOps.map(() => res.json); | ||
const result = resJSON.map((item) => ({ | ||
meta: res.meta, | ||
json: item, | ||
})); | ||
return result; | ||
}, | ||
}; | ||
}; | ||
const query = dataLoader(batchLoader('query')); | ||
const mutation = dataLoader(batchLoader('mutation')); | ||
const loaders = { query, mutation }; | ||
return ({ op }) => { | ||
return observable((observer) => { | ||
/* istanbul ignore if -- @preserve */ | ||
if (op.type === 'subscription') { | ||
throw new Error( | ||
'Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`', | ||
); | ||
} | ||
return requesterOpts.opts.headers; | ||
}, | ||
}); | ||
const loader = loaders[op.type]; | ||
const promise = loader.load(op); | ||
return { | ||
promise: promise.then((res) => { | ||
const resJSON = Array.isArray(res.json) | ||
? res.json | ||
: batchOps.map(() => res.json); | ||
let _res = undefined as HTTPResult | undefined; | ||
promise | ||
.then((res) => { | ||
_res = res; | ||
const transformed = transformResult( | ||
res.json, | ||
resolvedOpts.transformer.output, | ||
); | ||
const result = resJSON.map((item) => ({ | ||
meta: res.meta, | ||
json: item, | ||
})); | ||
if (!transformed.ok) { | ||
observer.error( | ||
TRPCClientError.from(transformed.error, { | ||
meta: res.meta, | ||
}), | ||
); | ||
return; | ||
} | ||
observer.next({ | ||
context: res.meta, | ||
result: transformed.result, | ||
}); | ||
observer.complete(); | ||
}) | ||
.catch((err) => { | ||
observer.error( | ||
TRPCClientError.from(err, { | ||
meta: _res?.meta, | ||
}), | ||
); | ||
}); | ||
return result; | ||
}), | ||
cancel, | ||
return () => { | ||
// noop | ||
}; | ||
}); | ||
}; | ||
}; | ||
}; | ||
export const httpBatchLink = createHTTPBatchLink(batchRequester); | ||
} |
@@ -1,16 +0,18 @@ | ||
import { NonEmptyArray } from '../internals/types'; | ||
import { HTTPLinkBaseOptions } from './internals/httpUtils'; | ||
import { HTTPHeaders, Operation } from './types'; | ||
import type { AnyClientTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { NonEmptyArray } from '../internals/types'; | ||
import type { HTTPLinkBaseOptions } from './internals/httpUtils'; | ||
import type { HTTPHeaders, Operation } from './types'; | ||
export interface HTTPBatchLinkOptions extends HTTPLinkBaseOptions { | ||
maxURLLength?: number; | ||
/** | ||
* Headers to be set on outgoing requests or a callback that of said headers | ||
* @link http://trpc.io/docs/client/headers | ||
*/ | ||
headers?: | ||
| HTTPHeaders | ||
| ((opts: { | ||
opList: NonEmptyArray<Operation>; | ||
}) => HTTPHeaders | Promise<HTTPHeaders>); | ||
} | ||
export type HTTPBatchLinkOptions<TRoot extends AnyClientTypes> = | ||
HTTPLinkBaseOptions<TRoot> & { | ||
maxURLLength?: number; | ||
/** | ||
* Headers to be set on outgoing requests or a callback that of said headers | ||
* @see http://trpc.io/docs/client/headers | ||
*/ | ||
headers?: | ||
| HTTPHeaders | ||
| ((opts: { | ||
opList: NonEmptyArray<Operation>; | ||
}) => HTTPHeaders | Promise<HTTPHeaders>); | ||
}; |
@@ -1,65 +0,195 @@ | ||
import { NonEmptyArray } from '../internals/types'; | ||
import { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import type { AnyRouter, ProcedureType } from '@trpc/server'; | ||
import { observable } from '@trpc/server/observable'; | ||
import type { TRPCErrorShape, TRPCResponse } from '@trpc/server/rpc'; | ||
import type { AnyRootTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
import { jsonlStreamConsumer } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { BatchLoader } from '../internals/dataLoader'; | ||
import { dataLoader } from '../internals/dataLoader'; | ||
import { allAbortSignals, raceAbortSignals } from '../internals/signals'; | ||
import type { NonEmptyArray } from '../internals/types'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import type { HTTPResult } from './internals/httpUtils'; | ||
import { | ||
createHTTPBatchLink, | ||
RequesterFn, | ||
} from './internals/createHTTPBatchLink'; | ||
import { getTextDecoder } from './internals/getTextDecoder'; | ||
import { streamingJsonHttpRequester } from './internals/parseJSONStream'; | ||
import { TextDecoderEsque } from './internals/streamingUtils'; | ||
import { Operation } from './types'; | ||
fetchHTTPResponse, | ||
getBody, | ||
getUrl, | ||
resolveHTTPLinkOptions, | ||
} from './internals/httpUtils'; | ||
import type { Operation, TRPCLink } from './types'; | ||
export interface HTTPBatchStreamLinkOptions extends HTTPBatchLinkOptions { | ||
/** | ||
* Will default to the webAPI `TextDecoder`, | ||
* but you can use this option if your client | ||
* runtime doesn't provide it. | ||
*/ | ||
textDecoder?: TextDecoderEsque; | ||
} | ||
export type HTTPBatchStreamLinkOptions<TRoot extends AnyRootTypes> = | ||
HTTPBatchLinkOptions<TRoot> & { | ||
/** | ||
* Maximum number of calls in a single batch request | ||
* @default Infinity | ||
*/ | ||
maxItems?: number; | ||
}; | ||
const streamRequester: RequesterFn<HTTPBatchStreamLinkOptions> = ( | ||
requesterOpts, | ||
) => { | ||
const textDecoder = getTextDecoder(requesterOpts.opts.textDecoder); | ||
return (batchOps, unitResolver) => { | ||
const path = batchOps.map((op) => op.path).join(','); | ||
const inputs = batchOps.map((op) => op.input); | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpBatchStreamLink | ||
*/ | ||
export function unstable_httpBatchStreamLink<TRouter extends AnyRouter>( | ||
opts: HTTPBatchStreamLinkOptions<TRouter['_def']['_config']['$types']>, | ||
): TRPCLink<TRouter> { | ||
const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
const maxURLLength = opts.maxURLLength ?? Infinity; | ||
const maxItems = opts.maxItems ?? Infinity; | ||
const { cancel, promise } = streamingJsonHttpRequester( | ||
{ | ||
...requesterOpts, | ||
textDecoder, | ||
path, | ||
inputs, | ||
headers() { | ||
if (!requesterOpts.opts.headers) { | ||
return {}; | ||
return () => { | ||
const batchLoader = ( | ||
type: ProcedureType, | ||
): BatchLoader<Operation, HTTPResult> => { | ||
return { | ||
validate(batchOps) { | ||
if (maxURLLength === Infinity && maxItems === Infinity) { | ||
// escape hatch for quick calcs | ||
return true; | ||
} | ||
if (typeof requesterOpts.opts.headers === 'function') { | ||
return requesterOpts.opts.headers({ | ||
opList: batchOps as NonEmptyArray<Operation>, | ||
}); | ||
if (batchOps.length > maxItems) { | ||
return false; | ||
} | ||
return requesterOpts.opts.headers; | ||
const path = batchOps.map((op) => op.path).join(','); | ||
const inputs = batchOps.map((op) => op.input); | ||
const url = getUrl({ | ||
...resolvedOpts, | ||
type, | ||
path, | ||
inputs, | ||
signal: null, | ||
}); | ||
return url.length <= maxURLLength; | ||
}, | ||
}, | ||
(index, res) => { | ||
unitResolver(index, res); | ||
}, | ||
); | ||
async fetch(batchOps) { | ||
const path = batchOps.map((op) => op.path).join(','); | ||
const inputs = batchOps.map((op) => op.input); | ||
return { | ||
/** | ||
* return an empty array because the batchLoader expects an array of results | ||
* but we've already called the `unitResolver` for each of them, there's | ||
* nothing left to do here. | ||
*/ | ||
promise: promise.then(() => []), | ||
cancel, | ||
const batchSignals = allAbortSignals( | ||
...batchOps.map((op) => op.signal), | ||
); | ||
const abortController = new AbortController(); | ||
const responsePromise = fetchHTTPResponse({ | ||
...resolvedOpts, | ||
signal: raceAbortSignals(batchSignals, abortController.signal), | ||
type, | ||
contentTypeHeader: 'application/json', | ||
trpcAcceptHeader: 'application/jsonl', | ||
getUrl, | ||
getBody, | ||
inputs, | ||
path, | ||
headers() { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
opList: batchOps as NonEmptyArray<Operation>, | ||
}); | ||
} | ||
return opts.headers; | ||
}, | ||
}); | ||
const res = await responsePromise; | ||
const [head] = await jsonlStreamConsumer< | ||
Record<string, Promise<any>> | ||
>({ | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
from: res.body!, | ||
deserialize: resolvedOpts.transformer.output.deserialize, | ||
// onError: console.error, | ||
formatError(opts) { | ||
const error = opts.error as TRPCErrorShape; | ||
return TRPCClientError.from({ | ||
error, | ||
}); | ||
}, | ||
abortController, | ||
}); | ||
const promises = Object.keys(batchOps).map( | ||
async (key): Promise<HTTPResult> => { | ||
let json: TRPCResponse = await Promise.resolve(head[key]); | ||
if ('result' in json) { | ||
/** | ||
* Not very pretty, but we need to unwrap nested data as promises | ||
* Our stream producer will only resolve top-level async values or async values that are directly nested in another async value | ||
*/ | ||
const result = await Promise.resolve(json.result); | ||
json = { | ||
result: { | ||
data: await Promise.resolve(result.data), | ||
}, | ||
}; | ||
} | ||
return { | ||
json, | ||
meta: { | ||
response: res, | ||
}, | ||
}; | ||
}, | ||
); | ||
return promises; | ||
}, | ||
}; | ||
}; | ||
const query = dataLoader(batchLoader('query')); | ||
const mutation = dataLoader(batchLoader('mutation')); | ||
const loaders = { query, mutation }; | ||
return ({ op }) => { | ||
return observable((observer) => { | ||
/* istanbul ignore if -- @preserve */ | ||
if (op.type === 'subscription') { | ||
throw new Error( | ||
'Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`', | ||
); | ||
} | ||
const loader = loaders[op.type]; | ||
const promise = loader.load(op); | ||
let _res = undefined as HTTPResult | undefined; | ||
promise | ||
.then((res) => { | ||
_res = res; | ||
if ('error' in res.json) { | ||
observer.error( | ||
TRPCClientError.from(res.json, { | ||
meta: res.meta, | ||
}), | ||
); | ||
return; | ||
} else if ('result' in res.json) { | ||
observer.next({ | ||
context: res.meta, | ||
result: res.json.result, | ||
}); | ||
observer.complete(); | ||
return; | ||
} | ||
observer.complete(); | ||
}) | ||
.catch((err) => { | ||
observer.error( | ||
TRPCClientError.from(err, { | ||
meta: _res?.meta, | ||
}), | ||
); | ||
}); | ||
return () => { | ||
// noop | ||
}; | ||
}); | ||
}; | ||
}; | ||
}; | ||
export const unstable_httpBatchStreamLink = | ||
createHTTPBatchLink(streamRequester); | ||
} |
@@ -1,86 +0,141 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { observable } from '@trpc/core/observable'; | ||
import { transformResult } from '../shared/transformResult'; | ||
import { observable } from '@trpc/server/observable'; | ||
import type { | ||
AnyClientTypes, | ||
AnyRouter, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { transformResult } from '@trpc/server/unstable-core-do-not-import'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import { | ||
import type { | ||
HTTPLinkBaseOptions, | ||
HTTPResult, | ||
Requester, | ||
} from './internals/httpUtils'; | ||
import { | ||
getInput, | ||
getUrl, | ||
httpRequest, | ||
jsonHttpRequester, | ||
Requester, | ||
resolveHTTPLinkOptions, | ||
} from './internals/httpUtils'; | ||
import { HTTPHeaders, Operation, TRPCLink } from './types'; | ||
import { | ||
isFormData, | ||
isOctetType, | ||
type HTTPHeaders, | ||
type Operation, | ||
type TRPCLink, | ||
} from './types'; | ||
export interface HTTPLinkOptions extends HTTPLinkBaseOptions { | ||
/** | ||
* Headers to be set on outgoing requests or a callback that of said headers | ||
* @link http://trpc.io/docs/client/headers | ||
*/ | ||
headers?: | ||
| HTTPHeaders | ||
| ((opts: { op: Operation }) => HTTPHeaders | Promise<HTTPHeaders>); | ||
} | ||
export type HTTPLinkOptions<TRoot extends AnyClientTypes> = | ||
HTTPLinkBaseOptions<TRoot> & { | ||
/** | ||
* Headers to be set on outgoing requests or a callback that of said headers | ||
* @see http://trpc.io/docs/client/headers | ||
*/ | ||
headers?: | ||
| HTTPHeaders | ||
| ((opts: { op: Operation }) => HTTPHeaders | Promise<HTTPHeaders>); | ||
}; | ||
export function httpLinkFactory(factoryOpts: { requester: Requester }) { | ||
return <TRouter extends AnyRouter>( | ||
opts: HTTPLinkOptions, | ||
): TRPCLink<TRouter> => { | ||
const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
const universalRequester: Requester = (opts) => { | ||
const input = getInput(opts); | ||
return (runtime) => | ||
({ op }) => | ||
observable((observer) => { | ||
const { path, input, type } = op; | ||
const { promise, cancel } = factoryOpts.requester({ | ||
...resolvedOpts, | ||
runtime, | ||
type, | ||
path, | ||
input, | ||
headers() { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
op, | ||
}); | ||
} | ||
return opts.headers; | ||
}, | ||
}); | ||
let meta: HTTPResult['meta'] | undefined = undefined; | ||
promise | ||
.then((res) => { | ||
meta = res.meta; | ||
const transformed = transformResult(res.json, runtime); | ||
if (isFormData(input)) { | ||
if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') { | ||
throw new Error('FormData is only supported for mutations'); | ||
} | ||
if (!transformed.ok) { | ||
observer.error( | ||
TRPCClientError.from(transformed.error, { | ||
meta, | ||
}), | ||
); | ||
return; | ||
} | ||
observer.next({ | ||
context: res.meta, | ||
result: transformed.result, | ||
return httpRequest({ | ||
...opts, | ||
// The browser will set this automatically and include the boundary= in it | ||
contentTypeHeader: undefined, | ||
getUrl, | ||
getBody: () => input, | ||
}); | ||
} | ||
if (isOctetType(input)) { | ||
if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') { | ||
throw new Error('Octet type input is only supported for mutations'); | ||
} | ||
return httpRequest({ | ||
...opts, | ||
contentTypeHeader: 'application/octet-stream', | ||
getUrl, | ||
getBody: () => input, | ||
}); | ||
} | ||
return jsonHttpRequester(opts); | ||
}; | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpLink | ||
*/ | ||
export function httpLink<TRouter extends AnyRouter = AnyRouter>( | ||
opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>, | ||
): TRPCLink<TRouter> { | ||
const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
return () => { | ||
return ({ op }) => { | ||
return observable((observer) => { | ||
const { path, input, type } = op; | ||
/* istanbul ignore if -- @preserve */ | ||
if (type === 'subscription') { | ||
throw new Error( | ||
'Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`', | ||
); | ||
} | ||
const request = universalRequester({ | ||
...resolvedOpts, | ||
type, | ||
path, | ||
input, | ||
signal: op.signal, | ||
headers() { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
op, | ||
}); | ||
observer.complete(); | ||
}) | ||
.catch((cause) => { | ||
observer.error(TRPCClientError.from(cause, { meta })); | ||
} | ||
return opts.headers; | ||
}, | ||
}); | ||
let meta: HTTPResult['meta'] | undefined = undefined; | ||
request | ||
.then((res) => { | ||
meta = res.meta; | ||
const transformed = transformResult( | ||
res.json, | ||
resolvedOpts.transformer.output, | ||
); | ||
if (!transformed.ok) { | ||
observer.error( | ||
TRPCClientError.from(transformed.error, { | ||
meta, | ||
}), | ||
); | ||
return; | ||
} | ||
observer.next({ | ||
context: res.meta, | ||
result: transformed.result, | ||
}); | ||
observer.complete(); | ||
}) | ||
.catch((cause) => { | ||
observer.error(TRPCClientError.from(cause, { meta })); | ||
}); | ||
return () => { | ||
cancel(); | ||
}; | ||
}); | ||
return () => { | ||
// noop | ||
}; | ||
}); | ||
}; | ||
}; | ||
} | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpLink | ||
*/ | ||
export const httpLink = httpLinkFactory({ requester: jsonHttpRequester }); |
@@ -1,4 +0,8 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { observable } from '@trpc/core/observable'; | ||
import { Operation, OperationLink, OperationResultObservable } from '../types'; | ||
import { observable } from '@trpc/server/observable'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { | ||
Operation, | ||
OperationLink, | ||
OperationResultObservable, | ||
} from '../types'; | ||
@@ -5,0 +9,0 @@ /** @internal */ |
/* istanbul ignore file -- @preserve */ | ||
// We're not actually exporting this link | ||
import { AnyRouter } from '@trpc/core'; | ||
import { Observable, observable, share } from '@trpc/core/observable'; | ||
import { TRPCLink } from '../types'; | ||
import type { Observable } from '@trpc/server/observable'; | ||
import { observable, share } from '@trpc/server/observable'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { TRPCLink } from '../types'; | ||
@@ -7,0 +8,0 @@ /** |
@@ -1,8 +0,11 @@ | ||
import { ProcedureType } from '@trpc/core'; | ||
import { TRPCResponse } from '@trpc/core/rpc'; | ||
import type { | ||
AnyClientTypes, | ||
CombinedDataTransformer, | ||
Maybe, | ||
ProcedureType, | ||
TRPCAcceptHeader, | ||
TRPCResponse, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { getFetch } from '../../getFetch'; | ||
import { getAbortController } from '../../internals/getAbortController'; | ||
import { | ||
AbortControllerEsque, | ||
AbortControllerInstanceEsque, | ||
import type { | ||
FetchEsque, | ||
@@ -12,5 +15,5 @@ RequestInitEsque, | ||
} from '../../internals/types'; | ||
import { TRPCClientError } from '../../TRPCClientError'; | ||
import { TextDecoderEsque } from '../internals/streamingUtils'; | ||
import { HTTPHeaders, PromiseAndCancel, TRPCClientRuntime } from '../types'; | ||
import type { TransformerOptions } from '../../unstable-internals'; | ||
import { getTransformer } from '../../unstable-internals'; | ||
import type { HTTPHeaders } from '../types'; | ||
@@ -20,3 +23,5 @@ /** | ||
*/ | ||
export interface HTTPLinkBaseOptions { | ||
export type HTTPLinkBaseOptions< | ||
TRoot extends Pick<AnyClientTypes, 'transformer'>, | ||
> = { | ||
url: string | URL; | ||
@@ -28,6 +33,8 @@ /** | ||
/** | ||
* Add ponyfill for AbortController | ||
* Send all requests `as POST`s requests regardless of the procedure type | ||
* The HTTP handler must separately allow overriding the method. See: | ||
* @see https://trpc.io/docs/rpc | ||
*/ | ||
AbortController?: AbortControllerEsque | null; | ||
} | ||
methodOverride?: 'POST'; | ||
} & TransformerOptions<TRoot>; | ||
@@ -37,12 +44,14 @@ export interface ResolvedHTTPLinkOptions { | ||
fetch?: FetchEsque; | ||
AbortController: AbortControllerEsque | null; | ||
transformer: CombinedDataTransformer; | ||
methodOverride?: 'POST'; | ||
} | ||
export function resolveHTTPLinkOptions( | ||
opts: HTTPLinkBaseOptions, | ||
opts: HTTPLinkBaseOptions<AnyClientTypes>, | ||
): ResolvedHTTPLinkOptions { | ||
return { | ||
url: opts.url.toString().replace(/\/$/, ''), // Remove any trailing slashes | ||
url: opts.url.toString(), | ||
fetch: opts.fetch, | ||
AbortController: getAbortController(opts.AbortController), | ||
transformer: getTransformer(opts.transformer), | ||
methodOverride: opts.methodOverride, | ||
}; | ||
@@ -64,2 +73,3 @@ } | ||
mutation: 'POST', | ||
subscription: 'PATCH', | ||
} as const; | ||
@@ -76,10 +86,10 @@ | ||
type GetInputOptions = { | ||
runtime: TRPCClientRuntime; | ||
transformer: CombinedDataTransformer; | ||
} & ({ input: unknown } | { inputs: unknown[] }); | ||
function getInput(opts: GetInputOptions) { | ||
export function getInput(opts: GetInputOptions) { | ||
return 'input' in opts | ||
? opts.runtime.transformer.serialize(opts.input) | ||
? opts.transformer.input.serialize(opts.input) | ||
: arrayToDict( | ||
opts.inputs.map((_input) => opts.runtime.transformer.serialize(_input)), | ||
opts.inputs.map((_input) => opts.transformer.input.serialize(_input)), | ||
); | ||
@@ -92,2 +102,3 @@ } | ||
path: string; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -97,4 +108,5 @@ | ||
type GetBody = (opts: HTTPBaseRequestOptions) => RequestInitEsque['body']; | ||
export type ContentOptions = { | ||
batchModeHeader?: 'stream'; | ||
trpcAcceptHeader?: TRPCAcceptHeader; | ||
contentTypeHeader?: string; | ||
@@ -106,10 +118,17 @@ getUrl: GetUrl; | ||
export const getUrl: GetUrl = (opts) => { | ||
let url = opts.url + '/' + opts.path; | ||
const parts = opts.url.split('?') as [string, string?]; | ||
const base = parts[0].replace(/\/$/, ''); // Remove any trailing slashes | ||
let url = base + '/' + opts.path; | ||
const queryParts: string[] = []; | ||
if (parts[1]) { | ||
queryParts.push(parts[1]); | ||
} | ||
if ('inputs' in opts) { | ||
queryParts.push('batch=1'); | ||
} | ||
if (opts.type === 'query') { | ||
if (opts.type === 'query' || opts.type === 'subscription') { | ||
const input = getInput(opts); | ||
if (input !== undefined) { | ||
if (input !== undefined && opts.methodOverride !== 'POST') { | ||
queryParts.push(`input=${encodeURIComponent(JSON.stringify(input))}`); | ||
@@ -125,3 +144,3 @@ } | ||
export const getBody: GetBody = (opts) => { | ||
if (opts.type === 'query') { | ||
if (opts.type === 'query' && opts.methodOverride !== 'POST') { | ||
return undefined; | ||
@@ -137,3 +156,3 @@ } | ||
}, | ||
) => PromiseAndCancel<HTTPResult>; | ||
) => Promise<HTTPResult>; | ||
@@ -149,12 +168,43 @@ export const jsonHttpRequester: Requester = (opts) => { | ||
/** | ||
* Polyfill for DOMException with AbortError name | ||
*/ | ||
class AbortError extends Error { | ||
constructor() { | ||
const name = 'AbortError'; | ||
super(name); | ||
this.name = name; | ||
this.message = name; | ||
} | ||
} | ||
export type HTTPRequestOptions = ContentOptions & | ||
HTTPBaseRequestOptions & { | ||
headers: () => HTTPHeaders | Promise<HTTPHeaders>; | ||
TextDecoder?: TextDecoderEsque; | ||
}; | ||
export async function fetchHTTPResponse( | ||
opts: HTTPRequestOptions, | ||
ac?: AbortControllerInstanceEsque | null, | ||
) { | ||
/** | ||
* Polyfill for `signal.throwIfAborted()` | ||
* | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted | ||
*/ | ||
const throwIfAborted = (signal: Maybe<AbortSignal>) => { | ||
if (!signal?.aborted) { | ||
return; | ||
} | ||
// If available, use the native implementation | ||
signal.throwIfAborted?.(); | ||
// If we have `DOMException`, use it | ||
if (typeof DOMException !== 'undefined') { | ||
throw new DOMException('AbortError', 'AbortError'); | ||
} | ||
// Otherwise, use our own implementation | ||
throw new AbortError(); | ||
}; | ||
export async function fetchHTTPResponse(opts: HTTPRequestOptions) { | ||
throwIfAborted(opts.signal); | ||
const url = opts.getUrl(opts); | ||
@@ -170,6 +220,2 @@ const body = opts.getBody(opts); | ||
})(); | ||
/* istanbul ignore if -- @preserve */ | ||
if (type === 'subscription') { | ||
throw new Error('Subscriptions should use wsLink'); | ||
} | ||
const headers = { | ||
@@ -179,5 +225,5 @@ ...(opts.contentTypeHeader | ||
: {}), | ||
...(opts.batchModeHeader | ||
? { 'trpc-batch-mode': opts.batchModeHeader } | ||
: {}), | ||
...(opts.trpcAcceptHeader | ||
? { 'trpc-accept': opts.trpcAcceptHeader } | ||
: undefined), | ||
...resolvedHeaders, | ||
@@ -187,5 +233,5 @@ }; | ||
return getFetch(opts.fetch)(url, { | ||
method: METHOD[type], | ||
signal: ac?.signal, | ||
body: body, | ||
method: opts.methodOverride ?? METHOD[type], | ||
signal: opts.signal, | ||
body, | ||
headers, | ||
@@ -195,34 +241,18 @@ }); | ||
export function httpRequest( | ||
export async function httpRequest( | ||
opts: HTTPRequestOptions, | ||
): PromiseAndCancel<HTTPResult> { | ||
const ac = opts.AbortController ? new opts.AbortController() : null; | ||
): Promise<HTTPResult> { | ||
const meta = {} as HTTPResult['meta']; | ||
let done = false; | ||
const promise = new Promise<HTTPResult>((resolve, reject) => { | ||
fetchHTTPResponse(opts, ac) | ||
.then((_res) => { | ||
meta.response = _res; | ||
done = true; | ||
return _res.json(); | ||
}) | ||
.then((json) => { | ||
meta.responseJSON = json; | ||
resolve({ | ||
json: json as TRPCResponse, | ||
meta, | ||
}); | ||
}) | ||
.catch((err) => { | ||
done = true; | ||
reject(TRPCClientError.from(err, { meta })); | ||
}); | ||
}); | ||
const cancel = () => { | ||
if (!done) { | ||
ac?.abort(); | ||
} | ||
const res = await fetchHTTPResponse(opts); | ||
meta.response = res; | ||
const json = await res.json(); | ||
meta.responseJSON = json; | ||
return { | ||
json: json as TRPCResponse, | ||
meta, | ||
}; | ||
return { promise, cancel }; | ||
} |
@@ -8,6 +8,9 @@ /// <reference lib="dom.iterable" /> | ||
import { AnyRouter } from '@trpc/core'; | ||
import { observable, tap } from '@trpc/core/observable'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import { Operation, OperationResultEnvelope, TRPCLink } from './types'; | ||
import { observable, tap } from '@trpc/server/observable'; | ||
import type { | ||
AnyRouter, | ||
InferrableClientTypes, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import type { TRPCClientError } from '../TRPCClientError'; | ||
import type { Operation, OperationResultEnvelope, TRPCLink } from './types'; | ||
@@ -19,6 +22,8 @@ type ConsoleEsque = { | ||
type EnableFnOptions<TRouter extends AnyRouter> = | ||
type EnableFnOptions<TRouter extends InferrableClientTypes> = | ||
| { | ||
direction: 'down'; | ||
result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>; | ||
result: | ||
| OperationResultEnvelope<unknown, TRPCClientError<TRouter>> | ||
| TRPCClientError<TRouter>; | ||
} | ||
@@ -39,3 +44,5 @@ | (Operation & { | ||
direction: 'down'; | ||
result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>; | ||
result: | ||
| OperationResultEnvelope<unknown, TRPCClientError<TRouter>> | ||
| TRPCClientError<TRouter>; | ||
elapsedMs: number; | ||
@@ -55,2 +62,4 @@ } | ||
type ColorMode = 'ansi' | 'css' | 'none'; | ||
export interface LoggerLinkOptions<TRouter extends AnyRouter> { | ||
@@ -67,3 +76,8 @@ logger?: LoggerLinkFn<TRouter>; | ||
*/ | ||
colorMode?: 'ansi' | 'css'; | ||
colorMode?: ColorMode; | ||
/** | ||
* Include context in the log - defaults to false unless `colorMode` is 'css' | ||
*/ | ||
withContext?: boolean; | ||
} | ||
@@ -104,6 +118,7 @@ | ||
opts: LoggerLinkFnOptions<any> & { | ||
colorMode: 'ansi' | 'css'; | ||
colorMode: ColorMode; | ||
withContext?: boolean; | ||
}, | ||
) { | ||
const { direction, type, path, id, input } = opts; | ||
const { direction, type, withContext, path, id, input } = opts; | ||
@@ -113,3 +128,5 @@ const parts: string[] = []; | ||
if (opts.colorMode === 'ansi') { | ||
if (opts.colorMode === 'none') { | ||
parts.push(direction === 'up' ? '>>' : '<<', type, `#${id}`, path); | ||
} else if (opts.colorMode === 'ansi') { | ||
const [lightRegular, darkRegular] = palettes.ansi.regular[type]; | ||
@@ -128,19 +145,6 @@ const [lightBold, darkBold] = palettes.ansi.bold[type]; | ||
); | ||
if (direction === 'up') { | ||
args.push({ input: opts.input }); | ||
} else { | ||
args.push({ | ||
input: opts.input, | ||
// strip context from result cause it's too noisy in terminal wihtout collapse mode | ||
result: 'result' in opts.result ? opts.result.result : opts.result, | ||
elapsedMs: opts.elapsedMs, | ||
}); | ||
} | ||
return { parts, args }; | ||
} | ||
const [light, dark] = palettes.css[type]; | ||
const css = ` | ||
} else { | ||
// css color mode | ||
const [light, dark] = palettes.css[type]; | ||
const css = ` | ||
background-color: #${direction === 'up' ? light : dark}; | ||
@@ -151,14 +155,19 @@ color: ${direction === 'up' ? 'black' : 'white'}; | ||
parts.push( | ||
'%c', | ||
direction === 'up' ? '>>' : '<<', | ||
type, | ||
`#${id}`, | ||
`%c${path}%c`, | ||
'%O', | ||
); | ||
args.push(css, `${css}; font-weight: bold;`, `${css}; font-weight: normal;`); | ||
parts.push( | ||
'%c', | ||
direction === 'up' ? '>>' : '<<', | ||
type, | ||
`#${id}`, | ||
`%c${path}%c`, | ||
'%O', | ||
); | ||
args.push( | ||
css, | ||
`${css}; font-weight: bold;`, | ||
`${css}; font-weight: normal;`, | ||
); | ||
} | ||
if (direction === 'up') { | ||
args.push({ input, context: opts.context }); | ||
args.push(withContext ? { input, context: opts.context } : { input }); | ||
} else { | ||
@@ -169,3 +178,3 @@ args.push({ | ||
elapsedMs: opts.elapsedMs, | ||
context: opts.context, | ||
...(withContext && { context: opts.context }), | ||
}); | ||
@@ -182,5 +191,7 @@ } | ||
colorMode = 'css', | ||
withContext, | ||
}: { | ||
c?: ConsoleEsque; | ||
colorMode?: 'ansi' | 'css'; | ||
colorMode?: ColorMode; | ||
withContext?: boolean; | ||
}): LoggerLinkFn<TRouter> => | ||
@@ -197,2 +208,3 @@ (props) => { | ||
input, | ||
withContext, | ||
}); | ||
@@ -203,3 +215,4 @@ | ||
props.result && | ||
(props.result instanceof Error || 'error' in props.result.result) | ||
(props.result instanceof Error || | ||
('error' in props.result.result && props.result.result.error)) | ||
? 'error' | ||
@@ -212,3 +225,3 @@ : 'log'; | ||
/** | ||
* @see https://trpc.io/docs/client/links/loggerLink | ||
* @see https://trpc.io/docs/v11/client/links/loggerLink | ||
*/ | ||
@@ -222,3 +235,6 @@ export function loggerLink<TRouter extends AnyRouter = AnyRouter>( | ||
opts.colorMode ?? (typeof window === 'undefined' ? 'ansi' : 'css'); | ||
const { logger = defaultLogger({ c: opts.console, colorMode }) } = opts; | ||
const withContext = opts.withContext ?? colorMode === 'css'; | ||
const { | ||
logger = defaultLogger({ c: opts.console, colorMode, withContext }), | ||
} = opts; | ||
@@ -229,3 +245,3 @@ return () => { | ||
// -> | ||
enabled({ ...op, direction: 'up' }) && | ||
if (enabled({ ...op, direction: 'up' })) { | ||
logger({ | ||
@@ -235,9 +251,12 @@ ...op, | ||
}); | ||
} | ||
const requestStartTime = Date.now(); | ||
function logResult( | ||
result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>, | ||
result: | ||
| OperationResultEnvelope<unknown, TRPCClientError<TRouter>> | ||
| TRPCClientError<TRouter>, | ||
) { | ||
const elapsedMs = Date.now() - requestStartTime; | ||
enabled({ ...op, direction: 'down', result }) && | ||
if (enabled({ ...op, direction: 'down', result })) { | ||
logger({ | ||
@@ -249,2 +268,3 @@ ...op, | ||
}); | ||
} | ||
} | ||
@@ -251,0 +271,0 @@ return next(op) |
@@ -1,5 +0,5 @@ | ||
import { AnyRouter } from '@trpc/core'; | ||
import { observable } from '@trpc/core/observable'; | ||
import { observable } from '@trpc/server/observable'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import { createChain } from './internals/createChain'; | ||
import { Operation, TRPCLink } from './types'; | ||
import type { Operation, TRPCLink } from './types'; | ||
@@ -6,0 +6,0 @@ function asArray<TType>(value: TType | TType[]) { |
@@ -1,15 +0,17 @@ | ||
import { | ||
AnyRouter, | ||
CombinedDataTransformer, | ||
DataTransformer, | ||
} from '@trpc/core'; | ||
import { Observable, Observer } from '@trpc/core/observable'; | ||
import { TRPCResultMessage, TRPCSuccessResponse } from '@trpc/core/rpc'; | ||
import { ResponseEsque } from '../internals/types'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import type { Observable, Observer } from '@trpc/server/observable'; | ||
import type { | ||
InferrableClientTypes, | ||
Maybe, | ||
TRPCResultMessage, | ||
TRPCSuccessResponse, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import type { ResponseEsque } from '../internals/types'; | ||
import type { TRPCClientError } from '../TRPCClientError'; | ||
import type { TRPCConnectionState } from './internals/subscriptions'; | ||
/** | ||
* @internal | ||
*/ | ||
export type CancelFn = () => void; | ||
export { | ||
isNonJsonSerializable, | ||
isFormData, | ||
isOctetType, | ||
} from './internals/contentTypes'; | ||
@@ -19,10 +21,2 @@ /** | ||
*/ | ||
export type PromiseAndCancel<TValue> = { | ||
promise: Promise<TValue>; | ||
cancel: CancelFn; | ||
}; | ||
/** | ||
* @internal | ||
*/ | ||
export interface OperationContext extends Record<string, unknown> {} | ||
@@ -39,2 +33,3 @@ | ||
context: OperationContext; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -63,5 +58,3 @@ | ||
export interface TRPCClientRuntime { | ||
transformer: DataTransformer; | ||
// FIXME: we should be able to remove this - added as `withTRPC()` needs it, but we can have it as an extra option on SSR instead | ||
combinedTransformer: CombinedDataTransformer; | ||
// nothing here anymore | ||
} | ||
@@ -72,6 +65,7 @@ | ||
*/ | ||
export interface OperationResultEnvelope<TOutput> { | ||
export interface OperationResultEnvelope<TOutput, TError> { | ||
result: | ||
| TRPCResultMessage<TOutput>['result'] | ||
| TRPCSuccessResponse<TOutput>['result']; | ||
| TRPCSuccessResponse<TOutput>['result'] | ||
| TRPCConnectionState<TError>; | ||
context?: OperationContext; | ||
@@ -84,5 +78,8 @@ } | ||
export type OperationResultObservable< | ||
TRouter extends AnyRouter, | ||
TInferrable extends InferrableClientTypes, | ||
TOutput, | ||
> = Observable<OperationResultEnvelope<TOutput>, TRPCClientError<TRouter>>; | ||
> = Observable< | ||
OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, | ||
TRPCClientError<TInferrable> | ||
>; | ||
@@ -93,5 +90,8 @@ /** | ||
export type OperationResultObserver< | ||
TRouter extends AnyRouter, | ||
TInferrable extends InferrableClientTypes, | ||
TOutput, | ||
> = Observer<OperationResultEnvelope<TOutput>, TRPCClientError<TRouter>>; | ||
> = Observer< | ||
OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, | ||
TRPCClientError<TInferrable> | ||
>; | ||
@@ -102,3 +102,3 @@ /** | ||
export type OperationLink< | ||
TRouter extends AnyRouter, | ||
TInferrable extends InferrableClientTypes, | ||
TInput = unknown, | ||
@@ -108,4 +108,6 @@ TOutput = unknown, | ||
op: Operation<TInput>; | ||
next: (op: Operation<TInput>) => OperationResultObservable<TRouter, TOutput>; | ||
}) => OperationResultObservable<TRouter, TOutput>; | ||
next: ( | ||
op: Operation<TInput>, | ||
) => OperationResultObservable<TInferrable, TOutput>; | ||
}) => OperationResultObservable<TInferrable, TOutput>; | ||
@@ -115,4 +117,4 @@ /** | ||
*/ | ||
export type TRPCLink<TRouter extends AnyRouter> = ( | ||
export type TRPCLink<TInferrable extends InferrableClientTypes> = ( | ||
opts: TRPCClientRuntime, | ||
) => OperationLink<TRouter>; | ||
) => OperationLink<TInferrable>; |
@@ -1,9 +0,9 @@ | ||
import { | ||
import type { Observer, UnsubscribeFn } from '@trpc/server/observable'; | ||
import { behaviorSubject, observable } from '@trpc/server/observable'; | ||
import type { TRPCConnectionParamsMessage } from '@trpc/server/rpc'; | ||
import type { | ||
AnyRouter, | ||
inferClientTypes, | ||
inferRouterError, | ||
MaybePromise, | ||
ProcedureType, | ||
} from '@trpc/core'; | ||
import { observable, Observer, UnsubscribeFn } from '@trpc/core/observable'; | ||
import { | ||
TRPCClientIncomingMessage, | ||
@@ -14,6 +14,13 @@ TRPCClientIncomingRequest, | ||
TRPCResponseMessage, | ||
} from '@trpc/core/rpc'; | ||
import { transformResult } from '../shared/transformResult'; | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { transformResult } from '@trpc/server/unstable-core-do-not-import'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import { Operation, TRPCLink } from './types'; | ||
import type { TransformerOptions } from '../unstable-internals'; | ||
import { getTransformer } from '../unstable-internals'; | ||
import type { TRPCConnectionState } from './internals/subscriptions'; | ||
import { | ||
resultOf, | ||
type UrlOptionsWithConnectionParams, | ||
} from './internals/urlWithConnectionParams'; | ||
import type { Operation, TRPCLink } from './types'; | ||
@@ -35,8 +42,4 @@ const run = <TResult>(fn: () => TResult): TResult => fn(); | ||
export interface WebSocketClientOptions { | ||
export interface WebSocketClientOptions extends UrlOptionsWithConnectionParams { | ||
/** | ||
* The URL to connect to (can be a function that returns a URL) | ||
*/ | ||
url: string | (() => MaybePromise<string>); | ||
/** | ||
* Ponyfill which WebSocket implementation to use | ||
@@ -55,2 +58,6 @@ */ | ||
/** | ||
* Triggered when a WebSocket connection encounters an error | ||
*/ | ||
onError?: (evt?: Event) => void; | ||
/** | ||
* Triggered when a WebSocket connection is closed | ||
@@ -74,2 +81,21 @@ */ | ||
}; | ||
/** | ||
* Send ping messages to the server and kill the connection if no pong message is returned | ||
*/ | ||
keepAlive?: { | ||
/** | ||
* @default false | ||
*/ | ||
enabled: boolean; | ||
/** | ||
* Send a ping message every this many milliseconds | ||
* @default 5_000 | ||
*/ | ||
intervalMs?: number; | ||
/** | ||
* Close the WebSocket after this many milliseconds if the server does not respond | ||
* @default 1_000 | ||
*/ | ||
pongTimeoutMs?: number; | ||
}; | ||
} | ||
@@ -82,9 +108,13 @@ | ||
}; | ||
/** | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export function createWSClient(opts: WebSocketClientOptions) { | ||
const { | ||
url, | ||
WebSocket: WebSocketImpl = WebSocket, | ||
retryDelayMs: retryDelayFn = exponentialBackoff, | ||
onOpen, | ||
onClose, | ||
} = opts; | ||
@@ -110,3 +140,3 @@ const lazyOpts: LazyOptions = { | ||
type TCallbacks = WSCallbackObserver<AnyRouter, unknown>; | ||
type TRequest = { | ||
type WsRequest = { | ||
/** | ||
@@ -119,4 +149,8 @@ * Reference to the WebSocket instance this request was made to | ||
op: Operation; | ||
/** | ||
* The last event id that the client has received | ||
*/ | ||
lastEventId: string | undefined; | ||
}; | ||
const pendingRequests: Record<number | string, TRequest> = | ||
const pendingRequests: Record<number | string, WsRequest> = | ||
Object.create(null); | ||
@@ -149,2 +183,17 @@ let connectAttempt = 0; | ||
const initState: TRPCConnectionState<TRPCClientError<AnyRouter>> = | ||
activeConnection | ||
? { | ||
type: 'state', | ||
state: 'connecting', | ||
error: null, | ||
} | ||
: { | ||
type: 'state', | ||
state: 'idle', | ||
error: null, | ||
}; | ||
const connectionState = | ||
behaviorSubject<TRPCConnectionState<TRPCClientError<AnyRouter>>>(initState); | ||
/** | ||
@@ -155,3 +204,3 @@ * tries to send the list of messages | ||
if (!activeConnection) { | ||
activeConnection = createConnection(); | ||
reconnect(null); | ||
return; | ||
@@ -182,3 +231,3 @@ } | ||
} | ||
function tryReconnect(conn: Connection) { | ||
function tryReconnect(cause: Error | null) { | ||
if (!!connectTimer) { | ||
@@ -188,5 +237,4 @@ return; | ||
conn.state = 'connecting'; | ||
const timeout = retryDelayFn(connectAttempt++); | ||
reconnectInMs(timeout); | ||
reconnectInMs(timeout, cause); | ||
} | ||
@@ -201,5 +249,5 @@ function hasPendingRequests(conn?: Connection) { | ||
function reconnect() { | ||
function reconnect(cause: Error | null) { | ||
if (lazyOpts.enabled && !hasPendingRequests()) { | ||
// Skip reconnecting if there are pending requests and we're in lazy mode | ||
// Skip reconnecting if there aren't pending requests and we're in lazy mode | ||
return; | ||
@@ -209,9 +257,22 @@ } | ||
activeConnection = createConnection(); | ||
oldConnection && closeIfNoPending(oldConnection); | ||
if (oldConnection) { | ||
closeIfNoPending(oldConnection); | ||
} | ||
const currentState = connectionState.get(); | ||
if (currentState.state !== 'connecting') { | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'connecting', | ||
error: cause ? TRPCClientError.from(cause) : null, | ||
}); | ||
} | ||
} | ||
function reconnectInMs(ms: number) { | ||
function reconnectInMs(ms: number, cause: Error | null) { | ||
if (connectTimer) { | ||
return; | ||
} | ||
connectTimer = setTimeout(reconnect, ms); | ||
connectTimer = setTimeout(() => { | ||
reconnect(cause); | ||
}, ms); | ||
} | ||
@@ -225,7 +286,11 @@ | ||
} | ||
function resumeSubscriptionOnReconnect(req: TRequest) { | ||
function resumeSubscriptionOnReconnect(req: WsRequest) { | ||
if (outgoing.some((r) => r.id === req.op.id)) { | ||
return; | ||
} | ||
request(req.op, req.callbacks); | ||
request({ | ||
op: req.op, | ||
callbacks: req.callbacks, | ||
lastEventId: req.lastEventId, | ||
}); | ||
} | ||
@@ -244,5 +309,10 @@ | ||
if (!hasPendingRequests(activeConnection)) { | ||
if (!hasPendingRequests()) { | ||
activeConnection.ws?.close(); | ||
activeConnection = null; | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'idle', | ||
error: null, | ||
}); | ||
} | ||
@@ -253,2 +323,4 @@ }, lazyOpts.closeMs); | ||
function createConnection(): Connection { | ||
let pingTimeout: ReturnType<typeof setTimeout> | undefined = undefined; | ||
let pongTimeout: ReturnType<typeof setTimeout> | undefined = undefined; | ||
const self: Connection = { | ||
@@ -261,11 +333,61 @@ id: ++connectionIndex, | ||
const onError = () => { | ||
function destroy() { | ||
const noop = () => { | ||
// no-op | ||
}; | ||
const { ws } = self; | ||
if (ws) { | ||
ws.onclose = noop; | ||
ws.onerror = noop; | ||
ws.onmessage = noop; | ||
ws.onopen = noop; | ||
ws.close(); | ||
} | ||
self.state = 'closed'; | ||
if (self === activeConnection) { | ||
tryReconnect(self); | ||
} | ||
const onCloseOrError = (cause: Error | null) => { | ||
clearTimeout(pingTimeout); | ||
clearTimeout(pongTimeout); | ||
self.state = 'closed'; | ||
if (activeConnection === self) { | ||
// connection might have been replaced already | ||
tryReconnect(cause); | ||
} | ||
for (const [key, req] of Object.entries(pendingRequests)) { | ||
if (req.connection !== self) { | ||
continue; | ||
} | ||
// The connection was closed either unexpectedly or because of a reconnect | ||
if (req.type === 'subscription') { | ||
// Subscriptions will resume after we've reconnected | ||
resumeSubscriptionOnReconnect(req); | ||
} else { | ||
// Queries and mutations will error if interrupted | ||
delete pendingRequests[key]; | ||
req.callbacks.error?.( | ||
TRPCClientError.from(cause ?? new TRPCWebSocketClosedError()), | ||
); | ||
} | ||
} | ||
}; | ||
run(async () => { | ||
const urlString = typeof url === 'function' ? await url() : url; | ||
const ws = new WebSocketImpl(urlString); | ||
const onError = (evt?: Event) => { | ||
onCloseOrError(new TRPCWebSocketClosedError({ cause: evt })); | ||
opts.onError?.(evt); | ||
}; | ||
function connect(url: string) { | ||
if (opts.connectionParams) { | ||
// append `?connectionParams=1` when connection params are used | ||
const prefix = url.includes('?') ? '&' : '?'; | ||
url += prefix + 'connectionParams=1'; | ||
} | ||
const ws = new WebSocketImpl(url); | ||
self.ws = ws; | ||
@@ -276,14 +398,79 @@ | ||
ws.addEventListener('open', () => { | ||
/* istanbul ignore next -- @preserve */ | ||
if (activeConnection?.ws !== ws) { | ||
return; | ||
ws.onopen = () => { | ||
async function sendConnectionParams() { | ||
if (!opts.connectionParams) { | ||
return; | ||
} | ||
const connectMsg: TRPCConnectionParamsMessage = { | ||
method: 'connectionParams', | ||
data: await resultOf(opts.connectionParams), | ||
}; | ||
ws.send(JSON.stringify(connectMsg)); | ||
} | ||
connectAttempt = 0; | ||
self.state = 'open'; | ||
function handleKeepAlive() { | ||
if (!opts.keepAlive?.enabled) { | ||
return; | ||
} | ||
const { pongTimeoutMs = 1_000, intervalMs = 5_000 } = opts.keepAlive; | ||
onOpen?.(); | ||
dispatch(); | ||
}); | ||
ws.addEventListener('error', onError); | ||
const schedulePing = () => { | ||
const schedulePongTimeout = () => { | ||
pongTimeout = setTimeout(() => { | ||
const wasOpen = self.state === 'open'; | ||
destroy(); | ||
if (wasOpen) { | ||
opts.onClose?.(); | ||
} | ||
}, pongTimeoutMs); | ||
}; | ||
pingTimeout = setTimeout(() => { | ||
ws.send('PING'); | ||
schedulePongTimeout(); | ||
}, intervalMs); | ||
}; | ||
ws.addEventListener('message', () => { | ||
clearTimeout(pingTimeout); | ||
clearTimeout(pongTimeout); | ||
schedulePing(); | ||
}); | ||
schedulePing(); | ||
} | ||
run(async () => { | ||
/* istanbul ignore next -- @preserve */ | ||
if (activeConnection?.ws !== ws) { | ||
return; | ||
} | ||
handleKeepAlive(); | ||
await sendConnectionParams(); | ||
connectAttempt = 0; | ||
self.state = 'open'; | ||
// Update connection state | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'pending', | ||
error: null, | ||
}); | ||
opts.onOpen?.(); | ||
dispatch(); | ||
}).catch((cause: unknown) => { | ||
ws.close( | ||
// "Status codes in the range 3000-3999 are reserved for use by libraries, frameworks, and applications" | ||
3000, | ||
); | ||
onCloseOrError( | ||
new TRPCWebSocketClosedError({ | ||
message: 'Initialization error', | ||
cause, | ||
}), | ||
); | ||
}); | ||
}; | ||
ws.onerror = onError; | ||
const handleIncomingRequest = (req: TRPCClientIncomingRequest) => { | ||
@@ -295,3 +482,7 @@ if (self !== activeConnection) { | ||
if (req.method === 'reconnect') { | ||
reconnect(); | ||
reconnect( | ||
new TRPCWebSocketClosedError({ | ||
message: 'Server requested reconnect', | ||
}), | ||
); | ||
// notify subscribers | ||
@@ -314,10 +505,19 @@ for (const pendingReq of Object.values(pendingRequests)) { | ||
if (self === activeConnection && req.connection !== activeConnection) { | ||
// gracefully replace old connection with this | ||
const oldConn = req.connection; | ||
// gracefully replace old connection with a new connection | ||
req.connection = self; | ||
oldConn && closeIfNoPending(oldConn); | ||
} | ||
if (req.connection !== self) { | ||
// the connection has been replaced | ||
return; | ||
} | ||
if ( | ||
'result' in data && | ||
data.result.type === 'data' && | ||
typeof data.result.id === 'string' | ||
) { | ||
req.lastEventId = data.result.id; | ||
} | ||
if ( | ||
'result' in data && | ||
data.result.type === 'stopped' && | ||
@@ -329,3 +529,12 @@ activeConnection === self | ||
}; | ||
ws.addEventListener('message', ({ data }) => { | ||
ws.onmessage = (event) => { | ||
const { data } = event; | ||
if (data === 'PONG') { | ||
return; | ||
} | ||
if (data === 'PING') { | ||
ws.send('PONG'); | ||
return; | ||
} | ||
startLazyDisconnectTimer(); | ||
@@ -344,46 +553,30 @@ | ||
} | ||
}); | ||
}; | ||
ws.addEventListener('close', ({ code }) => { | ||
if (self.state === 'open') { | ||
onClose?.({ code }); | ||
} | ||
self.state = 'closed'; | ||
ws.onclose = (event) => { | ||
const wasOpen = self.state === 'open'; | ||
if (activeConnection === self) { | ||
// connection might have been replaced already | ||
tryReconnect(self); | ||
destroy(); | ||
onCloseOrError(new TRPCWebSocketClosedError({ cause: event })); | ||
if (wasOpen) { | ||
opts.onClose?.(event); | ||
} | ||
}; | ||
} | ||
for (const [key, req] of Object.entries(pendingRequests)) { | ||
if (req.connection !== self) { | ||
continue; | ||
} | ||
if (self.state === 'closed') { | ||
// If the connection was closed, we just call `complete()` on the request | ||
delete pendingRequests[key]; | ||
req.callbacks.complete?.(); | ||
continue; | ||
} | ||
// The connection was closed either unexpectedly or because of a reconnect | ||
if (req.type === 'subscription') { | ||
// Subscriptions will resume after we've reconnected | ||
resumeSubscriptionOnReconnect(req); | ||
} else { | ||
// Queries and mutations will error if interrupted | ||
delete pendingRequests[key]; | ||
req.callbacks.error?.( | ||
TRPCClientError.from( | ||
new TRPCWebSocketClosedError('WebSocket closed prematurely'), | ||
), | ||
); | ||
} | ||
} | ||
Promise.resolve(resultOf(opts.url)) | ||
.then(connect) | ||
.catch(() => { | ||
onCloseOrError(new Error('Failed to resolve url')); | ||
}); | ||
}).catch(onError); | ||
return self; | ||
} | ||
function request(op: Operation, callbacks: TCallbacks): UnsubscribeFn { | ||
function request(opts: { | ||
op: Operation; | ||
callbacks: TCallbacks; | ||
lastEventId: string | undefined; | ||
}): UnsubscribeFn { | ||
const { op, callbacks, lastEventId } = opts; | ||
const { type, input, path, id } = op; | ||
@@ -396,4 +589,6 @@ const envelope: TRPCRequestMessage = { | ||
path, | ||
lastEventId, | ||
}, | ||
}; | ||
pendingRequests[id] = { | ||
@@ -404,2 +599,3 @@ connection: null, | ||
op, | ||
lastEventId, | ||
}; | ||
@@ -428,2 +624,3 @@ | ||
} | ||
return { | ||
@@ -440,3 +637,5 @@ close: () => { | ||
TRPCClientError.from( | ||
new Error('Closed before connection was established'), | ||
new TRPCWebSocketClosedError({ | ||
message: 'Closed before connection was established', | ||
}), | ||
), | ||
@@ -446,3 +645,5 @@ ); | ||
} | ||
activeConnection && closeIfNoPending(activeConnection); | ||
if (activeConnection) { | ||
closeIfNoPending(activeConnection); | ||
} | ||
clearTimeout(connectTimer); | ||
@@ -456,12 +657,37 @@ connectTimer = undefined; | ||
}, | ||
/** | ||
* Reconnect to the WebSocket server | ||
*/ | ||
reconnect, | ||
connectionState: connectionState, | ||
}; | ||
} | ||
/** | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export type TRPCWebSocketClient = ReturnType<typeof createWSClient>; | ||
export interface WebSocketLinkOptions { | ||
/** | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export type WebSocketLinkOptions<TRouter extends AnyRouter> = { | ||
client: TRPCWebSocketClient; | ||
} | ||
} & TransformerOptions<inferClientTypes<TRouter>>; | ||
class TRPCWebSocketClosedError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
constructor(opts?: { cause?: unknown; message?: string }) { | ||
super( | ||
opts?.message ?? 'WebSocket closed', | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore https://github.com/tc39/proposal-error-cause | ||
{ | ||
cause: opts?.cause, | ||
}, | ||
); | ||
this.name = 'TRPCWebSocketClosedError'; | ||
@@ -473,8 +699,12 @@ Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype); | ||
/** | ||
* @see https://trpc.io/docs/client/links/wsLink | ||
* @see https://trpc.io/docs/v11/client/links/wsLink | ||
* @deprecated | ||
* 🙋♂️ **Contributors needed** to continue supporting WebSockets! | ||
* See https://github.com/trpc/trpc/issues/6109 | ||
*/ | ||
export function wsLink<TRouter extends AnyRouter>( | ||
opts: WebSocketLinkOptions, | ||
opts: WebSocketLinkOptions<TRouter>, | ||
): TRPCLink<TRouter> { | ||
return (runtime) => { | ||
const transformer = getTransformer(opts.transformer); | ||
return () => { | ||
const { client } = opts; | ||
@@ -485,10 +715,21 @@ return ({ op }) => { | ||
const input = runtime.transformer.serialize(op.input); | ||
const input = transformer.input.serialize(op.input); | ||
const unsub = client.request( | ||
{ type, path, input, id, context }, | ||
{ | ||
const connState = | ||
type === 'subscription' | ||
? client.connectionState.subscribe({ | ||
next(result) { | ||
observer.next({ | ||
result, | ||
context, | ||
}); | ||
}, | ||
}) | ||
: null; | ||
const unsubscribeRequest = client.request({ | ||
op: { type, path, input, id, context, signal: null }, | ||
callbacks: { | ||
error(err) { | ||
observer.error(err as TRPCClientError<any>); | ||
unsub(); | ||
observer.error(err); | ||
unsubscribeRequest(); | ||
}, | ||
@@ -498,4 +739,4 @@ complete() { | ||
}, | ||
next(message) { | ||
const transformed = transformResult(message, runtime); | ||
next(event) { | ||
const transformed = transformResult(event, transformer.output); | ||
@@ -513,3 +754,3 @@ if (!transformed.ok) { | ||
unsub(); | ||
unsubscribeRequest(); | ||
observer.complete(); | ||
@@ -519,5 +760,7 @@ } | ||
}, | ||
); | ||
lastEventId: undefined, | ||
}); | ||
return () => { | ||
unsub(); | ||
unsubscribeRequest(); | ||
connState?.unsubscribe(); | ||
}; | ||
@@ -524,0 +767,0 @@ }); |
@@ -0,11 +1,14 @@ | ||
import type { | ||
inferClientTypes, | ||
InferrableClientTypes, | ||
Maybe, | ||
TRPCErrorResponse, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { | ||
DefaultErrorShape, | ||
getCauseFromUnknown, | ||
inferErrorShape, | ||
isObject, | ||
Maybe, | ||
TRPCInferrable, | ||
} from '@trpc/core'; | ||
import { TRPCErrorResponse } from '@trpc/core/rpc'; | ||
type DefaultErrorShape, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
type inferErrorShape<TInferrable extends InferrableClientTypes> = | ||
inferClientTypes<TInferrable>['errorShape']; | ||
export interface TRPCClientErrorBase<TShape extends DefaultErrorShape> { | ||
@@ -16,3 +19,3 @@ readonly message: string; | ||
} | ||
export type TRPCClientErrorLike<TInferrable extends TRPCInferrable> = | ||
export type TRPCClientErrorLike<TInferrable extends InferrableClientTypes> = | ||
TRPCClientErrorBase<inferErrorShape<TInferrable>>; | ||
@@ -40,3 +43,13 @@ | ||
export class TRPCClientError<TRouterOrProcedure extends TRPCInferrable> | ||
function getMessageFromUnknownError(err: unknown, fallback: string): string { | ||
if (typeof err === 'string') { | ||
return err; | ||
} | ||
if (isObject(err) && typeof err['message'] === 'string') { | ||
return err['message']; | ||
} | ||
return fallback; | ||
} | ||
export class TRPCClientError<TRouterOrProcedure extends InferrableClientTypes> | ||
extends Error | ||
@@ -81,4 +94,4 @@ implements TRPCClientErrorBase<inferErrorShape<TRouterOrProcedure>> | ||
public static from<TRouterOrProcedure extends TRPCInferrable>( | ||
_cause: Error | TRPCErrorResponse<any>, | ||
public static from<TRouterOrProcedure extends InferrableClientTypes>( | ||
_cause: Error | TRPCErrorResponse<any> | object, | ||
opts: { meta?: Record<string, unknown> } = {}, | ||
@@ -104,14 +117,10 @@ ): TRPCClientError<TRouterOrProcedure> { | ||
} | ||
if (!(cause instanceof Error)) { | ||
return new TRPCClientError('Unknown error', { | ||
return new TRPCClientError( | ||
getMessageFromUnknownError(cause, 'Unknown error'), | ||
{ | ||
...opts, | ||
cause: cause as any, | ||
}); | ||
} | ||
return new TRPCClientError(cause.message, { | ||
...opts, | ||
cause: getCauseFromUnknown(cause), | ||
}); | ||
}, | ||
); | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
306650
149
8001
2
2
11
+ Addedtypescript@5.7.3(transitive)