@trpc/client
Advanced tools
Comparing version 11.0.0-alpha-tmp-issues-4129-lazy-routers.309 to 11.0.0-alpha-tmp-issues-4129-lazy-routers.317
{ | ||
"bundleSize": 46703, | ||
"bundleOrigSize": 65934, | ||
"bundleReduction": 29.17, | ||
"bundleSize": 66038, | ||
"bundleOrigSize": 83214, | ||
"bundleReduction": 20.64, | ||
"modules": [ | ||
{ | ||
"id": "/src/links/wsLink.ts", | ||
"size": 12058, | ||
"origSize": 13652, | ||
"size": 18016, | ||
"origSize": 20084, | ||
"renderedExports": [ | ||
@@ -16,9 +16,33 @@ "createWSClient", | ||
"dependents": [], | ||
"percent": 25.82, | ||
"reduction": 11.68 | ||
"percent": 27.28, | ||
"reduction": 10.3 | ||
}, | ||
{ | ||
"id": "/src/links/httpSubscriptionLink.ts", | ||
"size": 7788, | ||
"origSize": 7568, | ||
"renderedExports": [ | ||
"unstable_httpSubscriptionLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 11.79, | ||
"reduction": 0 | ||
}, | ||
{ | ||
"id": "/src/links/httpBatchStreamLink.ts", | ||
"size": 6017, | ||
"origSize": 6295, | ||
"renderedExports": [ | ||
"unstable_httpBatchStreamLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 9.11, | ||
"reduction": 4.42 | ||
}, | ||
{ | ||
"id": "/src/links/loggerLink.ts", | ||
"size": 5545, | ||
"origSize": 6496, | ||
"size": 5596, | ||
"origSize": 6946, | ||
"renderedExports": [ | ||
@@ -29,9 +53,9 @@ "loggerLink" | ||
"dependents": [], | ||
"percent": 11.87, | ||
"reduction": 14.64 | ||
"percent": 8.47, | ||
"reduction": 19.44 | ||
}, | ||
{ | ||
"id": "/src/internals/dataLoader.ts", | ||
"size": 4409, | ||
"origSize": 4766, | ||
"size": 4084, | ||
"origSize": 4328, | ||
"renderedExports": [ | ||
@@ -42,28 +66,27 @@ "dataLoader" | ||
"dependents": [ | ||
"/src/links/internals/createHTTPBatchLink.ts" | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpBatchStreamLink.ts" | ||
], | ||
"percent": 9.44, | ||
"reduction": 7.49 | ||
"percent": 6.18, | ||
"reduction": 5.64 | ||
}, | ||
{ | ||
"id": "/src/links/internals/parseJSONStream.ts", | ||
"size": 4007, | ||
"origSize": 5037, | ||
"id": "/src/links/httpBatchLink.ts", | ||
"size": 3937, | ||
"origSize": 4170, | ||
"renderedExports": [ | ||
"parseJSONStream", | ||
"streamingJsonHttpRequester" | ||
"httpBatchLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpBatchStreamLink.ts" | ||
], | ||
"percent": 8.58, | ||
"reduction": 20.45 | ||
"dependents": [], | ||
"percent": 5.96, | ||
"reduction": 5.59 | ||
}, | ||
{ | ||
"id": "/src/links/internals/httpUtils.ts", | ||
"size": 3464, | ||
"origSize": 6007, | ||
"size": 3692, | ||
"origSize": 5873, | ||
"renderedExports": [ | ||
"resolveHTTPLinkOptions", | ||
"getInput", | ||
"getUrl", | ||
@@ -77,30 +100,26 @@ "getBody", | ||
"dependents": [ | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpLink.ts", | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpFormDataLink.ts", | ||
"/src/links/internals/createHTTPBatchLink.ts", | ||
"/src/links/internals/parseJSONStream.ts" | ||
"/src/links/httpBatchStreamLink.ts", | ||
"/src/links/httpSubscriptionLink.ts" | ||
], | ||
"percent": 7.42, | ||
"reduction": 42.33 | ||
"percent": 5.59, | ||
"reduction": 37.14 | ||
}, | ||
{ | ||
"id": "/src/links/internals/createHTTPBatchLink.ts", | ||
"size": 2944, | ||
"origSize": 3711, | ||
"id": "/src/links/httpLink.ts", | ||
"size": 3179, | ||
"origSize": 3707, | ||
"renderedExports": [ | ||
"createHTTPBatchLink" | ||
"httpLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpBatchStreamLink.ts" | ||
], | ||
"percent": 6.3, | ||
"reduction": 20.67 | ||
"dependents": [], | ||
"percent": 4.81, | ||
"reduction": 14.24 | ||
}, | ||
{ | ||
"id": "/src/internals/TRPCUntypedClient.ts", | ||
"size": 2299, | ||
"origSize": 4207, | ||
"size": 3158, | ||
"origSize": 4578, | ||
"renderedExports": [ | ||
@@ -114,24 +133,9 @@ "TRPCUntypedClient" | ||
], | ||
"percent": 4.92, | ||
"reduction": 45.35 | ||
"percent": 4.78, | ||
"reduction": 31.02 | ||
}, | ||
{ | ||
"id": "/src/links/httpLink.ts", | ||
"size": 2169, | ||
"origSize": 2753, | ||
"renderedExports": [ | ||
"httpLinkFactory", | ||
"httpLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpFormDataLink.ts" | ||
], | ||
"percent": 4.64, | ||
"reduction": 21.21 | ||
}, | ||
{ | ||
"id": "/src/TRPCClientError.ts", | ||
"size": 1866, | ||
"origSize": 3437, | ||
"size": 2787, | ||
"origSize": 3564, | ||
"renderedExports": [ | ||
@@ -143,27 +147,45 @@ "TRPCClientError" | ||
"/src/index.ts", | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpLink.ts", | ||
"/src/links/wsLink.ts", | ||
"/src/internals/TRPCUntypedClient.ts", | ||
"/src/links/internals/httpUtils.ts", | ||
"/src/links/internals/createHTTPBatchLink.ts" | ||
"/src/links/httpBatchStreamLink.ts", | ||
"/src/links/httpSubscriptionLink.ts", | ||
"/src/internals/TRPCUntypedClient.ts" | ||
], | ||
"percent": 4, | ||
"reduction": 45.71 | ||
"percent": 4.22, | ||
"reduction": 21.8 | ||
}, | ||
{ | ||
"id": "/src/links/httpBatchStreamLink.ts", | ||
"size": 1340, | ||
"origSize": 2176, | ||
"id": "/src/links/retryLink.ts", | ||
"size": 2194, | ||
"origSize": 2702, | ||
"renderedExports": [ | ||
"unstable_httpBatchStreamLink" | ||
"retryLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 2.87, | ||
"reduction": 38.42 | ||
"percent": 3.32, | ||
"reduction": 18.8 | ||
}, | ||
{ | ||
"id": "/src/internals/signals.ts", | ||
"size": 1188, | ||
"origSize": 1236, | ||
"renderedExports": [ | ||
"allAbortSignals", | ||
"raceAbortSignals" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpBatchLink.ts", | ||
"/src/links/httpBatchStreamLink.ts", | ||
"/src/links/httpSubscriptionLink.ts" | ||
], | ||
"percent": 1.8, | ||
"reduction": 3.88 | ||
}, | ||
{ | ||
"id": "/src/createTRPCClient.ts", | ||
"size": 1206, | ||
"origSize": 4369, | ||
"size": 1185, | ||
"origSize": 4694, | ||
"renderedExports": [ | ||
@@ -179,30 +201,6 @@ "clientCallTypeToProcedureType", | ||
], | ||
"percent": 2.58, | ||
"reduction": 72.4 | ||
"percent": 1.79, | ||
"reduction": 74.76 | ||
}, | ||
{ | ||
"id": "/src/links/httpBatchLink.ts", | ||
"size": 1205, | ||
"origSize": 1527, | ||
"renderedExports": [ | ||
"httpBatchLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 2.58, | ||
"reduction": 21.09 | ||
}, | ||
{ | ||
"id": "/src/links/httpFormDataLink.ts", | ||
"size": 703, | ||
"origSize": 772, | ||
"renderedExports": [ | ||
"experimental_formDataLink" | ||
], | ||
"removedExports": [], | ||
"dependents": [], | ||
"percent": 1.51, | ||
"reduction": 8.94 | ||
}, | ||
{ | ||
"id": "/src/links/internals/createChain.ts", | ||
@@ -219,3 +217,3 @@ "size": 690, | ||
], | ||
"percent": 1.48, | ||
"percent": 1.04, | ||
"reduction": 32.75 | ||
@@ -232,3 +230,3 @@ }, | ||
"dependents": [], | ||
"percent": 1.31, | ||
"percent": 0.92, | ||
"reduction": 44.95 | ||
@@ -239,3 +237,3 @@ }, | ||
"size": 565, | ||
"origSize": 1699, | ||
"origSize": 1697, | ||
"renderedExports": [ | ||
@@ -248,47 +246,63 @@ "getTransformer" | ||
], | ||
"percent": 1.21, | ||
"reduction": 66.75 | ||
"percent": 0.86, | ||
"reduction": 66.71 | ||
}, | ||
{ | ||
"id": "/src/links/internals/getTextDecoder.ts", | ||
"size": 553, | ||
"origSize": 634, | ||
"id": "/src/getFetch.ts", | ||
"size": 428, | ||
"origSize": 644, | ||
"renderedExports": [ | ||
"getTextDecoder" | ||
"getFetch" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/links/httpBatchStreamLink.ts" | ||
"/src/index.ts", | ||
"/src/links/internals/httpUtils.ts" | ||
], | ||
"percent": 1.18, | ||
"reduction": 12.78 | ||
"percent": 0.65, | ||
"reduction": 33.54 | ||
}, | ||
{ | ||
"id": "/src/internals/getAbortController.ts", | ||
"size": 542, | ||
"origSize": 710, | ||
"id": "/src/links/internals/contentTypes.ts", | ||
"size": 330, | ||
"origSize": 389, | ||
"renderedExports": [ | ||
"getAbortController" | ||
"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/links/internals/httpUtils.ts" | ||
"/src/links/httpSubscriptionLink.ts", | ||
"/src/links/retryLink.ts" | ||
], | ||
"percent": 1.16, | ||
"reduction": 23.66 | ||
"percent": 0.38, | ||
"reduction": 6.96 | ||
}, | ||
{ | ||
"id": "/src/getFetch.ts", | ||
"size": 428, | ||
"origSize": 644, | ||
"id": "/src/links/internals/urlWithConnectionParams.ts", | ||
"size": 240, | ||
"origSize": 1016, | ||
"renderedExports": [ | ||
"getFetch" | ||
"resultOf" | ||
], | ||
"removedExports": [], | ||
"dependents": [ | ||
"/src/index.ts", | ||
"/src/links/internals/httpUtils.ts" | ||
"/src/links/wsLink.ts", | ||
"/src/links/httpSubscriptionLink.ts" | ||
], | ||
"percent": 0.92, | ||
"reduction": 33.54 | ||
"percent": 0.36, | ||
"reduction": 76.38 | ||
}, | ||
@@ -306,3 +320,3 @@ { | ||
], | ||
"percent": 0.21, | ||
"percent": 0.15, | ||
"reduction": 82.58 | ||
@@ -313,3 +327,3 @@ }, | ||
"size": 0, | ||
"origSize": 588, | ||
"origSize": 652, | ||
"renderedExports": [], | ||
@@ -324,3 +338,3 @@ "removedExports": [], | ||
"size": 0, | ||
"origSize": 41, | ||
"origSize": 90, | ||
"renderedExports": [], | ||
@@ -330,2 +344,3 @@ "removedExports": [], | ||
"/src/links/wsLink.ts", | ||
"/src/links/httpSubscriptionLink.ts", | ||
"/src/links/internals/httpUtils.ts" | ||
@@ -337,3 +352,3 @@ ], | ||
], | ||
"moduleCount": 22 | ||
"moduleCount": 23 | ||
} |
import type { Unsubscribable } from '@trpc/server/observable'; | ||
import type { AnyProcedure, AnyRouter, inferClientTypes, inferProcedureInput, inferTransformedProcedureOutput, IntersectionError, ProcedureOptions, ProcedureType, RouterRecord } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { AnyProcedure, AnyRouter, inferClientTypes, inferProcedureInput, inferTransformedProcedureOutput, IntersectionError, 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 { TRPCProcedureOptions } from './internals/types'; | ||
import type { TRPCClientError } from './TRPCClientError'; | ||
@@ -17,5 +18,6 @@ /** | ||
}; | ||
type coerceAsyncGeneratorToIterable<T> = T extends AsyncGenerator<infer $T, infer $Return, infer $Next> ? AsyncIterable<$T, $Return, $Next> : T; | ||
/** @internal */ | ||
export type Resolver<TDef extends ResolverDef> = (input: TDef['input'], opts?: ProcedureOptions) => Promise<TDef['output']>; | ||
type SubscriptionResolver<TDef extends ResolverDef> = (input: TDef['input'], opts?: Partial<TRPCSubscriptionObserver<TDef['output'], TRPCClientError<TDef>>> & ProcedureOptions) => Unsubscribable; | ||
export type Resolver<TDef extends ResolverDef> = (input: TDef['input'], opts?: TRPCProcedureOptions) => Promise<coerceAsyncGeneratorToIterable<TDef['output']>>; | ||
type SubscriptionResolver<TDef extends ResolverDef> = (input: TDef['input'], opts: Partial<TRPCSubscriptionObserver<TDef['output'], TRPCClientError<TDef>>> & TRPCProcedureOptions) => Unsubscribable; | ||
type DecorateProcedure<TType extends ProcedureType, TDef extends ResolverDef> = TType extends 'query' ? { | ||
@@ -32,3 +34,3 @@ query: Resolver<TDef>; | ||
type DecoratedProcedureRecord<TRouter extends AnyRouter, TRecord extends RouterRecord> = { | ||
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends RouterRecord ? DecoratedProcedureRecord<TRouter, $Value> : $Value extends AnyProcedure ? DecorateProcedure<$Value['_def']['type'], { | ||
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyProcedure ? DecorateProcedure<$Value['_def']['type'], { | ||
input: inferProcedureInput<$Value>; | ||
@@ -38,3 +40,3 @@ output: inferTransformedProcedureOutput<inferClientTypes<TRouter>, $Value>; | ||
transformer: inferClientTypes<TRouter>['transformer']; | ||
}> : never : never; | ||
}> : $Value extends RouterRecord ? DecoratedProcedureRecord<TRouter, $Value> : never : never; | ||
}; | ||
@@ -41,0 +43,0 @@ /** @internal */ |
@@ -17,2 +17,10 @@ 'use strict'; | ||
*/ function createTRPCClientProxy(client) { | ||
const proxy = unstableCoreDoNotImport.createRecursiveProxy(({ path, args })=>{ | ||
const pathCopy = [ | ||
...path | ||
]; | ||
const procedureType = clientCallTypeToProcedureType(pathCopy.pop()); | ||
const fullPath = pathCopy.join('.'); | ||
return client[procedureType](fullPath, ...args); | ||
}); | ||
return unstableCoreDoNotImport.createFlatProxy((key)=>{ | ||
@@ -25,11 +33,3 @@ if (client.hasOwnProperty(key)) { | ||
} | ||
return unstableCoreDoNotImport.createRecursiveProxy(({ path , args })=>{ | ||
const pathCopy = [ | ||
key, | ||
...path | ||
]; | ||
const procedureType = clientCallTypeToProcedureType(pathCopy.pop()); | ||
const fullPath = pathCopy.join('.'); | ||
return client[procedureType](fullPath, ...args); | ||
}); | ||
return proxy[key]; | ||
}); | ||
@@ -36,0 +36,0 @@ } |
@@ -19,2 +19,3 @@ export * from './createTRPCUntypedClient'; | ||
type inferRouterClient as inferRouterProxyClient, } from './createTRPCClient'; | ||
export { type TRPCProcedureOptions } from './internals/types'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -7,2 +7,3 @@ 'use strict'; | ||
var TRPCClientError = require('./TRPCClientError.js'); | ||
var contentTypes = require('./links/internals/contentTypes.js'); | ||
var httpBatchLink = require('./links/httpBatchLink.js'); | ||
@@ -14,3 +15,4 @@ var httpBatchStreamLink = require('./links/httpBatchStreamLink.js'); | ||
var wsLink = require('./links/wsLink.js'); | ||
var httpFormDataLink = require('./links/httpFormDataLink.js'); | ||
var httpSubscriptionLink = require('./links/httpSubscriptionLink.js'); | ||
var retryLink = require('./links/retryLink.js'); | ||
var TRPCUntypedClient = require('./internals/TRPCUntypedClient.js'); | ||
@@ -28,6 +30,8 @@ | ||
exports.TRPCClientError = TRPCClientError.TRPCClientError; | ||
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.httpLinkFactory = httpLink.httpLinkFactory; | ||
exports.loggerLink = loggerLink.loggerLink; | ||
@@ -37,3 +41,4 @@ exports.splitLink = splitLink.splitLink; | ||
exports.wsLink = wsLink.wsLink; | ||
exports.experimental_formDataLink = httpFormDataLink.experimental_formDataLink; | ||
exports.unstable_httpSubscriptionLink = httpSubscriptionLink.unstable_httpSubscriptionLink; | ||
exports.retryLink = retryLink.retryLink; | ||
exports.TRPCUntypedClient = TRPCUntypedClient.TRPCUntypedClient; |
@@ -1,8 +0,4 @@ | ||
import type { 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 |
@@ -64,4 +64,3 @@ 'use strict'; | ||
const batch = { | ||
items, | ||
cancel: throwFatalError | ||
items | ||
}; | ||
@@ -71,16 +70,16 @@ for (const item of items){ | ||
} | ||
const unitResolver = (index, value)=>{ | ||
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; | ||
promise.then((result)=>{ | ||
for(let i = 0; i < result.length; i++){ | ||
const value = result[i]; | ||
unitResolver(i, value); | ||
} | ||
const promise = batchLoader.fetch(batch.items.map((_item)=>_item.key)); | ||
promise.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); | ||
} | ||
item.batch = null; | ||
item.reject = null; | ||
item.resolve = null; | ||
})); | ||
for (const item of batch.items){ | ||
@@ -117,14 +116,3 @@ item.reject?.(new Error('Missing result')); | ||
} | ||
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; | ||
} | ||
@@ -131,0 +119,0 @@ return { |
@@ -13,3 +13,3 @@ import type { AnyClientTypes, CombinedDataTransformer, DataTransformerOptions, TypeError } from '@trpc/server/unstable-core-do-not-import'; | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/v11/data-transformers | ||
* @see https://trpc.io/docs/v11/data-transformers | ||
**/ | ||
@@ -23,3 +23,3 @@ transformer: DataTransformerOptions; | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/v11/data-transformers | ||
* @see https://trpc.io/docs/v11/data-transformers | ||
**/ | ||
@@ -26,0 +26,0 @@ transformer?: TypeError<'You must define a transformer on your your `initTRPC`-object first'>; |
import type { Unsubscribable } from '@trpc/server/observable'; | ||
import type { AnyRouter, InferrableClientTypes, TypeError } from '@trpc/server/unstable-core-do-not-import'; | ||
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'; | ||
@@ -13,7 +14,10 @@ import { TRPCClientError } from '../TRPCClientError'; | ||
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; | ||
} | ||
@@ -20,0 +24,0 @@ /** @internal */ |
@@ -7,12 +7,23 @@ 'use strict'; | ||
function _define_property(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
} | ||
class TRPCUntypedClient { | ||
$request({ type , input , path , context ={} }) { | ||
$request(opts) { | ||
const chain$ = createChain.createChain({ | ||
links: this.links, | ||
op: { | ||
id: ++this.requestId, | ||
type, | ||
path, | ||
input, | ||
context | ||
...opts, | ||
context: opts.context ?? {}, | ||
id: ++this.requestId | ||
} | ||
@@ -22,14 +33,11 @@ }); | ||
} | ||
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; | ||
async requestAsPromise(opts) { | ||
try { | ||
const req$ = this.$request(opts); | ||
const envelope = await observable.observableToPromise(req$); | ||
const data = envelope.result.data; | ||
return data; | ||
} catch (err) { | ||
throw TRPCClientError.TRPCClientError.from(err); | ||
} | ||
} | ||
@@ -59,12 +67,31 @@ query(path, input, opts) { | ||
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.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; | ||
} | ||
} | ||
@@ -81,2 +108,5 @@ }, | ||
constructor(opts){ | ||
_define_property(this, "links", void 0); | ||
_define_property(this, "runtime", void 0); | ||
_define_property(this, "requestId", void 0); | ||
this.requestId = 0; | ||
@@ -83,0 +113,0 @@ this.runtime = {}; |
@@ -1,17 +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. | ||
@@ -43,3 +28,3 @@ * @see fetch from lib.dom.d.ts | ||
*/ | ||
body?: FormData | ReadableStream | string | null; | ||
body?: FormData | string | null | Uint8Array | Blob | File; | ||
/** | ||
@@ -56,3 +41,3 @@ * Sets the request's associated headers. | ||
*/ | ||
signal?: AbortSignal | null; | ||
signal?: AbortSignal | undefined; | ||
} | ||
@@ -87,2 +72,14 @@ /** | ||
export type NonEmptyArray<TItem> = [TItem, ...TItem[]]; | ||
type ClientContext = Record<string, unknown>; | ||
/** | ||
* @public | ||
*/ | ||
export interface TRPCProcedureOptions { | ||
/** | ||
* Client-side context | ||
*/ | ||
context?: ClientContext; | ||
signal?: AbortSignal; | ||
} | ||
export {}; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -9,3 +9,4 @@ export * from './links/types'; | ||
export * from './links/wsLink'; | ||
export * from './links/httpFormDataLink'; | ||
export * from './links/httpSubscriptionLink'; | ||
export * from './links/retryLink'; | ||
//# sourceMappingURL=links.d.ts.map |
@@ -0,3 +1,8 @@ | ||
import type { AnyRouter } from '@trpc/server'; | ||
import type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
export declare const httpBatchLink: <TRouter extends import("@trpc/server/unstable-core-do-not-import").AnyRouter>(opts: HTTPBatchLinkOptions<import("@trpc/server/unstable-core-do-not-import").inferClientTypes<TRouter>>) => import("./types").TRPCLink<TRouter>; | ||
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'; | ||
var createHTTPBatchLink = require('./internals/createHTTPBatchLink.js'); | ||
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'); | ||
const batchRequester = (requesterOpts)=>{ | ||
return (batchOps)=>{ | ||
const path = batchOps.map((op)=>op.path).join(','); | ||
const inputs = batchOps.map((op)=>op.input); | ||
const { promise , cancel } = httpUtils.jsonHttpRequester({ | ||
...requesterOpts, | ||
path, | ||
inputs, | ||
headers () { | ||
if (!requesterOpts.opts.headers) { | ||
return {}; | ||
/** | ||
* @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; | ||
} | ||
if (typeof requesterOpts.opts.headers === 'function') { | ||
return requesterOpts.opts.headers({ | ||
opList: batchOps | ||
}; | ||
}; | ||
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 | ||
}); | ||
} | ||
return requesterOpts.opts.headers; | ||
} | ||
}); | ||
return { | ||
promise: promise.then((res)=>{ | ||
const resJSON = Array.isArray(res.json) ? res.json : batchOps.map(()=>res.json); | ||
const result = resJSON.map((item)=>({ | ||
meta: res.meta, | ||
json: item | ||
observer.complete(); | ||
}).catch((err)=>{ | ||
observer.error(TRPCClientError.TRPCClientError.from(err, { | ||
meta: _res?.meta | ||
})); | ||
return result; | ||
}), | ||
cancel | ||
}); | ||
return ()=>{ | ||
// noop | ||
}; | ||
}); | ||
}; | ||
}; | ||
}; | ||
const httpBatchLink = createHTTPBatchLink.createHTTPBatchLink(batchRequester); | ||
} | ||
exports.httpBatchLink = httpBatchLink; |
@@ -9,3 +9,3 @@ import type { AnyClientTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
* 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,0 +12,0 @@ headers?: HTTPHeaders | ((opts: { |
@@ -0,13 +1,16 @@ | ||
import type { AnyRouter } from '@trpc/server'; | ||
import type { AnyRootTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions'; | ||
import type { TextDecoderEsque } from './internals/streamingUtils'; | ||
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; | ||
maxItems?: number; | ||
}; | ||
export declare const unstable_httpBatchStreamLink: <TRouter extends import("@trpc/server/unstable-core-do-not-import").AnyRouter>(opts: HTTPBatchLinkOptions<import("@trpc/server/unstable-core-do-not-import").inferClientTypes<TRouter>>) => import("./types").TRPCLink<TRouter>; | ||
/** | ||
* @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 |
'use strict'; | ||
var createHTTPBatchLink = require('./internals/createHTTPBatchLink.js'); | ||
var getTextDecoder = require('./internals/getTextDecoder.js'); | ||
var parseJSONStream = require('./internals/parseJSONStream.js'); | ||
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'); | ||
const streamRequester = (requesterOpts)=>{ | ||
const textDecoder = getTextDecoder.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 } = parseJSONStream.streamingJsonHttpRequester({ | ||
...requesterOpts, | ||
textDecoder, | ||
path, | ||
inputs, | ||
headers () { | ||
if (!requesterOpts.opts.headers) { | ||
return {}; | ||
} | ||
if (typeof requesterOpts.opts.headers === 'function') { | ||
return requesterOpts.opts.headers({ | ||
opList: batchOps | ||
/** | ||
* @see https://trpc.io/docs/client/links/httpBatchStreamLink | ||
*/ function unstable_httpBatchStreamLink(opts) { | ||
const resolvedOpts = httpUtils.resolveHTTPLinkOptions(opts); | ||
const maxURLLength = opts.maxURLLength ?? Infinity; | ||
const maxItems = opts.maxItems ?? Infinity; | ||
return ()=>{ | ||
const batchLoader = (type)=>{ | ||
return { | ||
validate (batchOps) { | ||
if (maxURLLength === Infinity && maxItems === Infinity) { | ||
// escape hatch for quick calcs | ||
return true; | ||
} | ||
if (batchOps.length > maxItems) { | ||
return false; | ||
} | ||
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 batchSignals = signals.allAbortSignals(...batchOps.map((op)=>op.signal)); | ||
const abortController = new AbortController(); | ||
const responsePromise = httpUtils.fetchHTTPResponse({ | ||
...resolvedOpts, | ||
signal: signals.raceAbortSignals(batchSignals, abortController.signal), | ||
type, | ||
contentTypeHeader: 'application/json', | ||
trpcAcceptHeader: 'application/jsonl', | ||
getUrl: httpUtils.getUrl, | ||
getBody: httpUtils.getBody, | ||
inputs, | ||
path, | ||
headers () { | ||
if (!opts.headers) { | ||
return {}; | ||
} | ||
if (typeof opts.headers === 'function') { | ||
return opts.headers({ | ||
opList: batchOps | ||
}); | ||
} | ||
return opts.headers; | ||
} | ||
}); | ||
const res = await responsePromise; | ||
const [head] = await unstableCoreDoNotImport.jsonlStreamConsumer({ | ||
// 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; | ||
return TRPCClientError.TRPCClientError.from({ | ||
error | ||
}); | ||
}, | ||
abortController | ||
}); | ||
const promises = Object.keys(batchOps).map(async (key)=>{ | ||
let json = 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; | ||
} | ||
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 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 `httpBatchStreamLink` - use `httpSubscriptionLink` or `wsLink`'); | ||
} | ||
const loader = loaders[op.type]; | ||
const promise = loader.load(op); | ||
let _res = undefined; | ||
promise.then((res)=>{ | ||
_res = res; | ||
if ('error' in res.json) { | ||
observer.error(TRPCClientError.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.TRPCClientError.from(err, { | ||
meta: _res?.meta | ||
})); | ||
}); | ||
return ()=>{ | ||
// noop | ||
}; | ||
}); | ||
}; | ||
}; | ||
}; | ||
const unstable_httpBatchStreamLink = createHTTPBatchLink.createHTTPBatchLink(streamRequester); | ||
} | ||
exports.unstable_httpBatchStreamLink = unstable_httpBatchStreamLink; |
@@ -1,8 +0,8 @@ | ||
import type { AnyRootTypes, AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { HTTPLinkBaseOptions, Requester } from './internals/httpUtils'; | ||
import type { HTTPHeaders, Operation, TRPCLink } from './types'; | ||
export type HTTPLinkOptions<TRoot extends AnyRootTypes> = HTTPLinkBaseOptions<TRoot> & { | ||
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 | ||
*/ | ||
@@ -13,9 +13,6 @@ headers?: HTTPHeaders | ((opts: { | ||
}; | ||
export declare function httpLinkFactory(factoryOpts: { | ||
requester: Requester; | ||
}): <TRouter extends AnyRouter>(opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>) => TRPCLink<TRouter>; | ||
/** | ||
* @link https://trpc.io/docs/v11/client/links/httpLink | ||
* @see https://trpc.io/docs/client/links/httpLink | ||
*/ | ||
export declare const httpLink: <TRouter extends AnyRouter>(opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>) => TRPCLink<TRouter>; | ||
export declare function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>): TRPCLink<TRouter>; | ||
//# sourceMappingURL=httpLink.d.ts.map |
@@ -7,58 +7,88 @@ 'use strict'; | ||
var httpUtils = require('./internals/httpUtils.js'); | ||
var contentTypes = require('./internals/contentTypes.js'); | ||
function httpLinkFactory(factoryOpts) { | ||
return (opts)=>{ | ||
const resolvedOpts = httpUtils.resolveHTTPLinkOptions(opts); | ||
return ()=>({ op })=>observable.observable((observer)=>{ | ||
const { path , input , type } = op; | ||
const { promise , cancel } = factoryOpts.requester({ | ||
...resolvedOpts, | ||
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 = unstableCoreDoNotImport.transformResult(res.json, resolvedOpts.transformer.output); | ||
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 | ||
}; | ||
}); | ||
}; | ||
}; | ||
} | ||
/** | ||
* @link https://trpc.io/docs/v11/client/links/httpLink | ||
*/ const httpLink = httpLinkFactory({ | ||
requester: httpUtils.jsonHttpRequester | ||
}); | ||
exports.httpLink = httpLink; | ||
exports.httpLinkFactory = httpLinkFactory; |
@@ -1,10 +0,9 @@ | ||
import type { AnyRootTypes, CombinedDataTransformer, ProcedureType, TRPCResponse } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { AbortControllerEsque, AbortControllerInstanceEsque, FetchEsque, RequestInitEsque, ResponseEsque } from '../../internals/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 { TextDecoderEsque } from '../internals/streamingUtils'; | ||
import type { HTTPHeaders, PromiseAndCancel } from '../types'; | ||
import type { HTTPHeaders } from '../types'; | ||
/** | ||
* @internal | ||
*/ | ||
export type HTTPLinkBaseOptions<TRoot extends Pick<AnyRootTypes, 'transformer'>> = { | ||
export type HTTPLinkBaseOptions<TRoot extends Pick<AnyClientTypes, 'transformer'>> = { | ||
url: string | URL; | ||
@@ -16,9 +15,5 @@ /** | ||
/** | ||
* Add ponyfill for AbortController | ||
*/ | ||
AbortController?: AbortControllerEsque | null; | ||
/** | ||
* Send all requests `as POST`s requests regardless of the procedure type | ||
* The HTTP handler must separately allow overriding the method. See: | ||
* @link https://trpc.io/docs/rpc | ||
* @see https://trpc.io/docs/rpc | ||
*/ | ||
@@ -30,7 +25,6 @@ methodOverride?: 'POST'; | ||
fetch?: FetchEsque; | ||
AbortController: AbortControllerEsque | null; | ||
transformer: CombinedDataTransformer; | ||
methodOverride?: 'POST'; | ||
} | ||
export declare function resolveHTTPLinkOptions(opts: HTTPLinkBaseOptions<AnyRootTypes>): ResolvedHTTPLinkOptions; | ||
export declare function resolveHTTPLinkOptions(opts: HTTPLinkBaseOptions<AnyClientTypes>): ResolvedHTTPLinkOptions; | ||
export interface HTTPResult { | ||
@@ -50,5 +44,7 @@ json: TRPCResponse; | ||
}); | ||
export declare function getInput(opts: GetInputOptions): any; | ||
export type HTTPBaseRequestOptions = GetInputOptions & ResolvedHTTPLinkOptions & { | ||
type: ProcedureType; | ||
path: string; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -58,3 +54,3 @@ type GetUrl = (opts: HTTPBaseRequestOptions) => string; | ||
export type ContentOptions = { | ||
batchModeHeader?: 'stream'; | ||
trpcAcceptHeader?: TRPCAcceptHeader; | ||
contentTypeHeader?: string; | ||
@@ -68,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 |
'use strict'; | ||
var getFetch = require('../../getFetch.js'); | ||
var getAbortController = require('../../internals/getAbortController.js'); | ||
var TRPCClientError = require('../../TRPCClientError.js'); | ||
var transformer = require('../../internals/transformer.js'); | ||
@@ -10,5 +8,4 @@ | ||
return { | ||
url: opts.url.toString().replace(/\/$/, ''), | ||
url: opts.url.toString(), | ||
fetch: opts.fetch, | ||
AbortController: getAbortController.getAbortController(opts.AbortController), | ||
transformer: transformer.getTransformer(opts.transformer), | ||
@@ -29,3 +26,4 @@ methodOverride: opts.methodOverride | ||
query: 'GET', | ||
mutation: 'POST' | ||
mutation: 'POST', | ||
subscription: 'PATCH' | ||
}; | ||
@@ -36,8 +34,13 @@ function getInput(opts) { | ||
const getUrl = (opts)=>{ | ||
let url = opts.url + '/' + opts.path; | ||
const parts = opts.url.split('?'); | ||
const base = parts[0].replace(/\/$/, ''); // Remove any trailing slashes | ||
let url = base + '/' + opts.path; | ||
const queryParts = []; | ||
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); | ||
@@ -68,6 +71,34 @@ if (input !== undefined && opts.methodOverride !== 'POST') { | ||
}; | ||
async function fetchHTTPResponse(opts, ac) { | ||
/** | ||
* Polyfill for DOMException with AbortError name | ||
*/ class AbortError extends Error { | ||
constructor(){ | ||
const name = 'AbortError'; | ||
super(name); | ||
this.name = name; | ||
this.message = name; | ||
} | ||
} | ||
/** | ||
* Polyfill for `signal.throwIfAborted()` | ||
* | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted | ||
*/ const throwIfAborted = (signal)=>{ | ||
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(); | ||
}; | ||
async function fetchHTTPResponse(opts) { | ||
throwIfAborted(opts.signal); | ||
const url = opts.getUrl(opts); | ||
const body = opts.getBody(opts); | ||
const { type } = opts; | ||
const { type } = opts; | ||
const resolvedHeaders = await (async ()=>{ | ||
@@ -80,5 +111,2 @@ const heads = await opts.headers(); | ||
})(); | ||
/* istanbul ignore if -- @preserve */ if (type === 'subscription') { | ||
throw new Error('Subscriptions should use wsLink'); | ||
} | ||
const headers = { | ||
@@ -88,5 +116,5 @@ ...opts.contentTypeHeader ? { | ||
} : {}, | ||
...opts.batchModeHeader ? { | ||
'trpc-batch-mode': opts.batchModeHeader | ||
} : {}, | ||
...opts.trpcAcceptHeader ? { | ||
'trpc-accept': opts.trpcAcceptHeader | ||
} : undefined, | ||
...resolvedHeaders | ||
@@ -96,3 +124,3 @@ }; | ||
method: opts.methodOverride ?? METHOD[type], | ||
signal: ac?.signal, | ||
signal: opts.signal, | ||
body, | ||
@@ -102,32 +130,11 @@ headers | ||
} | ||
function httpRequest(opts) { | ||
const ac = opts.AbortController ? new opts.AbortController() : null; | ||
async function httpRequest(opts) { | ||
const meta = {}; | ||
let done = false; | ||
const promise = new Promise((resolve, reject)=>{ | ||
fetchHTTPResponse(opts, ac).then((_res)=>{ | ||
meta.response = _res; | ||
done = true; | ||
return _res.json(); | ||
}).then((json)=>{ | ||
meta.responseJSON = json; | ||
resolve({ | ||
json: json, | ||
meta | ||
}); | ||
}).catch((err)=>{ | ||
done = true; | ||
reject(TRPCClientError.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 { | ||
promise, | ||
cancel | ||
json: json, | ||
meta | ||
}; | ||
@@ -138,2 +145,3 @@ } | ||
exports.getBody = getBody; | ||
exports.getInput = getInput; | ||
exports.getUrl = getUrl; | ||
@@ -140,0 +148,0 @@ exports.httpRequest = httpRequest; |
@@ -1,3 +0,2 @@ | ||
/// <reference lib="dom.iterable" /> | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { AnyRouter, InferrableClientTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { TRPCClientError } from '../TRPCClientError'; | ||
@@ -9,5 +8,5 @@ import type { Operation, OperationResultEnvelope, TRPCLink } from './types'; | ||
}; | ||
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; | ||
} | ||
/** | ||
* @link https://trpc.io/docs/v11/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>; |
@@ -67,6 +67,8 @@ 'use strict'; | ||
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]; | ||
@@ -76,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}; | ||
@@ -100,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 | ||
}); | ||
@@ -113,3 +103,5 @@ } else { | ||
elapsedMs: opts.elapsedMs, | ||
context: opts.context | ||
...withContext && { | ||
context: opts.context | ||
} | ||
}); | ||
@@ -123,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, [ | ||
@@ -138,34 +131,40 @@ parts.join(' ') | ||
/** | ||
* @link https://trpc.io/docs/v11/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 | ||
}); | ||
} | ||
} | ||
@@ -172,0 +171,0 @@ return next(op).pipe(observable.tap({ |
import type { Observable, Observer } from '@trpc/server/observable'; | ||
import type { InferrableClientTypes, TRPCResultMessage, TRPCSuccessResponse } from '@trpc/server/unstable-core-do-not-import'; | ||
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> { | ||
@@ -30,2 +21,3 @@ } | ||
context: OperationContext; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -49,4 +41,4 @@ interface HeadersInitEsque { | ||
*/ | ||
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; | ||
@@ -57,7 +49,7 @@ } | ||
*/ | ||
export type OperationResultObservable<TInferrable extends InferrableClientTypes, TOutput> = Observable<OperationResultEnvelope<TOutput>, TRPCClientError<TInferrable>>; | ||
export type OperationResultObservable<TInferrable extends InferrableClientTypes, TOutput> = Observable<OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, TRPCClientError<TInferrable>>; | ||
/** | ||
* @internal | ||
*/ | ||
export type OperationResultObserver<TInferrable extends InferrableClientTypes, TOutput> = Observer<OperationResultEnvelope<TOutput>, TRPCClientError<TInferrable>>; | ||
export type OperationResultObserver<TInferrable extends InferrableClientTypes, TOutput> = Observer<OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, TRPCClientError<TInferrable>>; | ||
/** | ||
@@ -74,3 +66,2 @@ * @internal | ||
export type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>; | ||
export {}; | ||
//# sourceMappingURL=types.d.ts.map |
import type { Observer, UnsubscribeFn } from '@trpc/server/observable'; | ||
import type { AnyRouter, inferClientTypes, inferRouterError, MaybePromise, TRPCResponseMessage } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { AnyRouter, inferClientTypes, inferRouterError, TRPCResponseMessage } from '@trpc/server/unstable-core-do-not-import'; | ||
import { TRPCClientError } from '../TRPCClientError'; | ||
import type { TransformerOptions } from '../unstable-internals'; | ||
import type { TRPCConnectionState } from './internals/subscriptions'; | ||
import { type UrlOptionsWithConnectionParams } from './internals/urlWithConnectionParams'; | ||
import type { Operation, TRPCLink } from './types'; | ||
@@ -9,8 +11,4 @@ type WSCallbackResult<TRouter extends AnyRouter, TOutput> = TRPCResponseMessage<TOutput, inferRouterError<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 | ||
@@ -29,2 +27,6 @@ */ | ||
/** | ||
* Triggered when a WebSocket connection encounters an error | ||
*/ | ||
onError?: (evt?: Event) => void; | ||
/** | ||
* Triggered when a WebSocket connection is closed | ||
@@ -50,20 +52,66 @@ */ | ||
}; | ||
/** | ||
* 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>; | ||
/** | ||
* @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> = { | ||
@@ -73,3 +121,6 @@ client: TRPCWebSocketClient; | ||
/** | ||
* @link https://trpc.io/docs/v11/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 | ||
*/ | ||
@@ -76,0 +127,0 @@ export declare function wsLink<TRouter extends AnyRouter>(opts: WebSocketLinkOptions<TRouter>): TRPCLink<TRouter>; |
@@ -7,2 +7,3 @@ 'use strict'; | ||
var transformer = require('../internals/transformer.js'); | ||
var urlWithConnectionParams = require('./internals/urlWithConnectionParams.js'); | ||
@@ -15,4 +16,9 @@ const run = (fn)=>fn(); | ||
}; | ||
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 = { | ||
@@ -34,2 +40,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); | ||
/** | ||
@@ -39,3 +55,3 @@ * tries to send the list of messages | ||
if (!activeConnection) { | ||
activeConnection = createConnection(); | ||
reconnect(null); | ||
return; | ||
@@ -65,9 +81,8 @@ } | ||
} | ||
function tryReconnect(conn) { | ||
function tryReconnect(cause) { | ||
if (!!connectTimer) { | ||
return; | ||
} | ||
conn.state = 'connecting'; | ||
const timeout = retryDelayFn(connectAttempt++); | ||
reconnectInMs(timeout); | ||
reconnectInMs(timeout, cause); | ||
} | ||
@@ -81,5 +96,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; | ||
@@ -89,9 +104,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); | ||
} | ||
@@ -108,3 +135,7 @@ function closeIfNoPending(conn) { | ||
} | ||
request(req.op, req.callbacks); | ||
request({ | ||
op: req.op, | ||
callbacks: req.callbacks, | ||
lastEventId: req.lastEventId | ||
}); | ||
} | ||
@@ -120,5 +151,10 @@ const startLazyDisconnectTimer = ()=>{ | ||
} | ||
if (!hasPendingRequests(activeConnection)) { | ||
if (!hasPendingRequests()) { | ||
activeConnection.ws?.close(); | ||
activeConnection = null; | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'idle', | ||
error: null | ||
}); | ||
} | ||
@@ -128,2 +164,4 @@ }, lazyOpts.closeMs); | ||
function createConnection() { | ||
let pingTimeout = undefined; | ||
let pongTimeout = undefined; | ||
const self = { | ||
@@ -134,24 +172,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)=>{ | ||
@@ -162,3 +295,5 @@ if (self !== activeConnection) { | ||
if (req.method === 'reconnect') { | ||
reconnect(); | ||
reconnect(new TRPCWebSocketClosedError({ | ||
message: 'Server requested reconnect' | ||
})); | ||
// notify subscribers | ||
@@ -180,7 +315,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) { | ||
@@ -190,3 +330,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(); | ||
@@ -203,40 +351,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 = { | ||
@@ -247,3 +377,4 @@ id, | ||
input, | ||
path | ||
path, | ||
lastEventId | ||
} | ||
@@ -255,3 +386,4 @@ }; | ||
callbacks, | ||
op | ||
op, | ||
lastEventId | ||
}; | ||
@@ -284,6 +416,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); | ||
@@ -296,8 +432,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'; | ||
@@ -308,43 +452,60 @@ Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype); | ||
/** | ||
* @link https://trpc.io/docs/v11/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) { | ||
const transformer$1 = transformer.getTransformer(opts.transformer); | ||
return ()=>{ | ||
const { client } = opts; | ||
return ({ op })=>{ | ||
const { client } = opts; | ||
return ({ op })=>{ | ||
return observable.observable((observer)=>{ | ||
const { type , path , id , context } = op; | ||
const { type, path, id, context } = op; | ||
const input = transformer$1.input.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 = unstableCoreDoNotImport.transformResult(message, transformer$1.output); | ||
if (!transformed.ok) { | ||
observer.error(TRPCClientError.TRPCClientError.from(transformed.error)); | ||
return; | ||
} | ||
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 +512,0 @@ }); |
@@ -24,3 +24,3 @@ import type { inferClientTypes, InferrableClientTypes, Maybe, TRPCErrorResponse } from '@trpc/server/unstable-core-do-not-import'; | ||
}); | ||
static from<TRouterOrProcedure extends InferrableClientTypes>(_cause: Error | TRPCErrorResponse<any>, opts?: { | ||
static from<TRouterOrProcedure extends InferrableClientTypes>(_cause: Error | TRPCErrorResponse<any> | object, opts?: { | ||
meta?: Record<string, unknown>; | ||
@@ -27,0 +27,0 @@ }): TRPCClientError<TRouterOrProcedure>; |
@@ -5,2 +5,15 @@ 'use strict'; | ||
function _define_property(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
} | ||
function isTRPCClientError(cause) { | ||
@@ -15,2 +28,11 @@ return cause instanceof TRPCClientError || /** | ||
} | ||
function getMessageFromUnknownError(err, fallback) { | ||
if (typeof err === 'string') { | ||
return err; | ||
} | ||
if (unstableCoreDoNotImport.isObject(err) && typeof err['message'] === 'string') { | ||
return err['message']; | ||
} | ||
return fallback; | ||
} | ||
class TRPCClientError extends Error { | ||
@@ -35,11 +57,5 @@ static from(_cause, opts = {}) { | ||
} | ||
if (!(cause instanceof Error)) { | ||
return new TRPCClientError('Unknown error', { | ||
...opts, | ||
cause: cause | ||
}); | ||
} | ||
return new TRPCClientError(cause.message, { | ||
return new TRPCClientError(getMessageFromUnknownError(cause, 'Unknown error'), { | ||
...opts, | ||
cause: unstableCoreDoNotImport.getCauseFromUnknown(cause) | ||
cause: cause | ||
}); | ||
@@ -53,3 +69,8 @@ } | ||
cause | ||
}); | ||
}), // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore override doesn't work in all environments due to "This member cannot have an 'override' modifier because it is not declared in the base class 'Error'" | ||
_define_property(this, "cause", void 0), _define_property(this, "shape", void 0), _define_property(this, "data", void 0), /** | ||
* Additional meta data about the error | ||
* In the case of HTTP-errors, we'll have `response` and potentially `responseJSON` here | ||
*/ _define_property(this, "meta", void 0); | ||
this.meta = opts?.meta; | ||
@@ -56,0 +77,0 @@ this.cause = cause; |
export * from './internals/transformer'; | ||
export * from './links/internals/subscriptions'; | ||
//# sourceMappingURL=unstable-internals.d.ts.map |
{ | ||
"name": "@trpc/client", | ||
"version": "11.0.0-alpha-tmp-issues-4129-lazy-routers.309+ba5a1e47c", | ||
"version": "11.0.0-alpha-tmp-issues-4129-lazy-routers.317+b8473d2da", | ||
"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 src", | ||
"lint": "eslint --cache src", | ||
"ts-watch": "tsc --watch" | ||
@@ -77,17 +77,21 @@ }, | ||
"unstable-internals", | ||
"!**/*.test.*" | ||
"!**/*.test.*", | ||
"!**/__tests__" | ||
], | ||
"peerDependencies": { | ||
"@trpc/server": "11.0.0-alpha-tmp-issues-4129-lazy-routers.309+ba5a1e47c" | ||
"@trpc/server": "11.0.0-alpha-tmp-issues-4129-lazy-routers.317+b8473d2da", | ||
"typescript": ">=5.7.2" | ||
}, | ||
"devDependencies": { | ||
"@trpc/server": "11.0.0-alpha-tmp-issues-4129-lazy-routers.309+ba5a1e47c", | ||
"@trpc/server": "11.0.0-alpha-tmp-issues-4129-lazy-routers.317+b8473d2da", | ||
"@types/isomorphic-fetch": "^0.0.39", | ||
"@types/node": "^20.10.0", | ||
"eslint": "^8.56.0", | ||
"@types/node": "^22.9.0", | ||
"eslint": "^9.13.0", | ||
"isomorphic-fetch": "^3.0.0", | ||
"node-fetch": "^3.3.0", | ||
"rollup": "^4.9.5", | ||
"rollup": "^4.24.4", | ||
"tslib": "^2.8.1", | ||
"tsx": "^4.0.0", | ||
"undici": "^6.0.1" | ||
"typescript": "^5.7.2", | ||
"undici": "^7.0.0" | ||
}, | ||
@@ -100,3 +104,3 @@ "publishConfig": { | ||
], | ||
"gitHead": "ba5a1e47cdc7be0e5233dfca4d6272eb3ac71a29" | ||
"gitHead": "b8473d2da5f1c796b402bd9aa84b796266f9e3b5" | ||
} |
@@ -10,3 +10,2 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
IntersectionError, | ||
ProcedureOptions, | ||
ProcedureType, | ||
@@ -25,2 +24,3 @@ RouterRecord, | ||
import { TRPCUntypedClient } from './internals/TRPCUntypedClient'; | ||
import type { TRPCProcedureOptions } from './internals/types'; | ||
import type { TRPCClientError } from './TRPCClientError'; | ||
@@ -41,14 +41,19 @@ | ||
type coerceAsyncGeneratorToIterable<T> = | ||
T extends AsyncGenerator<infer $T, infer $Return, infer $Next> | ||
? AsyncIterable<$T, $Return, $Next> | ||
: T; | ||
/** @internal */ | ||
export type Resolver<TDef extends ResolverDef> = ( | ||
input: TDef['input'], | ||
opts?: ProcedureOptions, | ||
) => Promise<TDef['output']>; | ||
opts?: TRPCProcedureOptions, | ||
) => Promise<coerceAsyncGeneratorToIterable<TDef['output']>>; | ||
type SubscriptionResolver<TDef extends ResolverDef> = ( | ||
input: TDef['input'], | ||
opts?: Partial< | ||
opts: Partial< | ||
TRPCSubscriptionObserver<TDef['output'], TRPCClientError<TDef>> | ||
> & | ||
ProcedureOptions, | ||
TRPCProcedureOptions, | ||
) => Unsubscribable; | ||
@@ -64,10 +69,10 @@ | ||
: TType extends 'mutation' | ||
? { | ||
mutate: Resolver<TDef>; | ||
} | ||
: TType extends 'subscription' | ||
? { | ||
subscribe: SubscriptionResolver<TDef>; | ||
} | ||
: never; | ||
? { | ||
mutate: Resolver<TDef>; | ||
} | ||
: TType extends 'subscription' | ||
? { | ||
subscribe: SubscriptionResolver<TDef>; | ||
} | ||
: never; | ||
@@ -82,5 +87,3 @@ /** | ||
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value | ||
? $Value extends RouterRecord | ||
? DecoratedProcedureRecord<TRouter, $Value> | ||
: $Value extends AnyProcedure | ||
? $Value extends AnyProcedure | ||
? DecorateProcedure< | ||
@@ -98,3 +101,5 @@ $Value['_def']['type'], | ||
> | ||
: never | ||
: $Value extends RouterRecord | ||
? DecoratedProcedureRecord<TRouter, $Value> | ||
: never | ||
: never; | ||
@@ -135,2 +140,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) => { | ||
@@ -143,10 +158,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]; | ||
}); | ||
@@ -153,0 +161,0 @@ } |
@@ -23,1 +23,3 @@ // TODO: Be explicit about what we export here | ||
} from './createTRPCClient'; | ||
export { type TRPCProcedureOptions } from './internals/types'; |
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
import type { 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 @@ |
@@ -20,3 +20,3 @@ import type { | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/v11/data-transformers | ||
* @see https://trpc.io/docs/v11/data-transformers | ||
**/ | ||
@@ -30,3 +30,3 @@ transformer: DataTransformerOptions; | ||
* You must use the same transformer on the backend and frontend | ||
* @link https://trpc.io/docs/v11/data-transformers | ||
* @see https://trpc.io/docs/v11/data-transformers | ||
**/ | ||
@@ -33,0 +33,0 @@ transformer?: TypeError<'You must define a transformer on your your `initTRPC`-object first'>; |
@@ -8,6 +8,9 @@ import type { | ||
AnyRouter, | ||
inferAsyncIterableYield, | ||
InferrableClientTypes, | ||
Maybe, | ||
TypeError, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { createChain } from '../links/internals/createChain'; | ||
import type { TRPCConnectionState } from '../links/internals/subscriptions'; | ||
import type { | ||
@@ -31,7 +34,8 @@ OperationContext, | ||
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; | ||
} | ||
@@ -70,8 +74,3 @@ | ||
private $request<TInput = unknown, TOutput = unknown>({ | ||
type, | ||
input, | ||
path, | ||
context = {}, | ||
}: { | ||
private $request<TInput = unknown, TOutput = unknown>(opts: { | ||
type: TRPCType; | ||
@@ -81,2 +80,3 @@ input: TInput; | ||
context?: OperationContext; | ||
signal: Maybe<AbortSignal>; | ||
}) { | ||
@@ -86,7 +86,5 @@ const chain$ = createChain<AnyRouter, TInput, TOutput>({ | ||
op: { | ||
...opts, | ||
context: opts.context ?? {}, | ||
id: ++this.requestId, | ||
type, | ||
path, | ||
input, | ||
context, | ||
}, | ||
@@ -96,3 +94,4 @@ }); | ||
} | ||
private requestAsPromise<TInput = unknown, TOutput = unknown>(opts: { | ||
private async requestAsPromise<TInput = unknown, TOutput = unknown>(opts: { | ||
type: TRPCType; | ||
@@ -102,21 +101,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); | ||
} | ||
} | ||
@@ -153,12 +145,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.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; | ||
} | ||
} | ||
@@ -165,0 +172,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; | ||
} | ||
@@ -109,1 +91,14 @@ | ||
export type NonEmptyArray<TItem> = [TItem, ...TItem[]]; | ||
type ClientContext = Record<string, unknown>; | ||
/** | ||
* @public | ||
*/ | ||
export interface TRPCProcedureOptions { | ||
/** | ||
* Client-side context | ||
*/ | ||
context?: ClientContext; | ||
signal?: AbortSignal; | ||
} |
@@ -10,6 +10,6 @@ export * from './links/types'; | ||
export * from './links/wsLink'; | ||
export * from './links/httpFormDataLink'; | ||
export * from './links/httpSubscriptionLink'; | ||
export * from './links/retryLink'; | ||
// These are not public (yet) as we get this functionality from tanstack query | ||
// export * from './links/internals/retryLink'; | ||
// export * from './links/internals/dedupeLink'; |
@@ -1,51 +0,137 @@ | ||
import type { AnyRootTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
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 { RequesterFn } from './internals/createHTTPBatchLink'; | ||
import { createHTTPBatchLink } from './internals/createHTTPBatchLink'; | ||
import { jsonHttpRequester } from './internals/httpUtils'; | ||
import type { Operation } from './types'; | ||
import type { HTTPResult } from './internals/httpUtils'; | ||
import { | ||
getUrl, | ||
jsonHttpRequester, | ||
resolveHTTPLinkOptions, | ||
} from './internals/httpUtils'; | ||
import type { Operation, TRPCLink } from './types'; | ||
const batchRequester: RequesterFn<HTTPBatchLinkOptions<AnyRootTypes>> = ( | ||
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); | ||
} |
@@ -11,3 +11,3 @@ import type { AnyClientTypes } from '@trpc/server/unstable-core-do-not-import'; | ||
* 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 | ||
*/ | ||
@@ -14,0 +14,0 @@ headers?: |
@@ -0,10 +1,20 @@ | ||
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 { RequesterFn } from './internals/createHTTPBatchLink'; | ||
import { createHTTPBatchLink } from './internals/createHTTPBatchLink'; | ||
import { getTextDecoder } from './internals/getTextDecoder'; | ||
import { streamingJsonHttpRequester } from './internals/parseJSONStream'; | ||
import type { TextDecoderEsque } from './internals/streamingUtils'; | ||
import type { Operation } from './types'; | ||
import type { HTTPResult } from './internals/httpUtils'; | ||
import { | ||
fetchHTTPResponse, | ||
getBody, | ||
getUrl, | ||
resolveHTTPLinkOptions, | ||
} from './internals/httpUtils'; | ||
import type { Operation, TRPCLink } from './types'; | ||
@@ -14,53 +24,173 @@ export type HTTPBatchStreamLinkOptions<TRoot extends AnyRootTypes> = | ||
/** | ||
* 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; | ||
maxItems?: number; | ||
}; | ||
const streamRequester: RequesterFn<HTTPBatchStreamLinkOptions<AnyRootTypes>> = ( | ||
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 `httpBatchStreamLink` - 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); | ||
} |
import { observable } from '@trpc/server/observable'; | ||
import type { | ||
AnyRootTypes, | ||
AnyClientTypes, | ||
AnyRouter, | ||
@@ -14,12 +14,21 @@ } from '@trpc/server/unstable-core-do-not-import'; | ||
import { | ||
getInput, | ||
getUrl, | ||
httpRequest, | ||
jsonHttpRequester, | ||
resolveHTTPLinkOptions, | ||
} from './internals/httpUtils'; | ||
import type { HTTPHeaders, Operation, TRPCLink } from './types'; | ||
import { | ||
isFormData, | ||
isOctetType, | ||
type HTTPHeaders, | ||
type Operation, | ||
type TRPCLink, | ||
} from './types'; | ||
export type HTTPLinkOptions<TRoot extends AnyRootTypes> = | ||
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 | ||
*/ | ||
@@ -31,66 +40,104 @@ headers?: | ||
export function httpLinkFactory(factoryOpts: { requester: Requester }) { | ||
return <TRouter extends AnyRouter>( | ||
opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>, | ||
): TRPCLink<TRouter> => { | ||
const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
const universalRequester: Requester = (opts) => { | ||
const input = getInput(opts); | ||
return () => | ||
({ op }) => | ||
observable((observer) => { | ||
const { path, input, type } = op; | ||
const { promise, cancel } = factoryOpts.requester({ | ||
...resolvedOpts, | ||
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, | ||
resolvedOpts.transformer.output, | ||
); | ||
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 | ||
}; | ||
}); | ||
}; | ||
}; | ||
} | ||
/** | ||
* @link https://trpc.io/docs/v11/client/links/httpLink | ||
*/ | ||
export const httpLink = httpLinkFactory({ requester: jsonHttpRequester }); |
import type { | ||
AnyRootTypes, | ||
AnyClientTypes, | ||
CombinedDataTransformer, | ||
Maybe, | ||
ProcedureType, | ||
TRPCAcceptHeader, | ||
TRPCResponse, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import { getFetch } from '../../getFetch'; | ||
import { getAbortController } from '../../internals/getAbortController'; | ||
import type { | ||
AbortControllerEsque, | ||
AbortControllerInstanceEsque, | ||
FetchEsque, | ||
@@ -16,7 +15,5 @@ RequestInitEsque, | ||
} from '../../internals/types'; | ||
import { TRPCClientError } from '../../TRPCClientError'; | ||
import type { TransformerOptions } from '../../unstable-internals'; | ||
import { getTransformer } from '../../unstable-internals'; | ||
import type { TextDecoderEsque } from '../internals/streamingUtils'; | ||
import type { HTTPHeaders, PromiseAndCancel } from '../types'; | ||
import type { HTTPHeaders } from '../types'; | ||
@@ -27,3 +24,3 @@ /** | ||
export type HTTPLinkBaseOptions< | ||
TRoot extends Pick<AnyRootTypes, 'transformer'>, | ||
TRoot extends Pick<AnyClientTypes, 'transformer'>, | ||
> = { | ||
@@ -36,9 +33,5 @@ url: string | URL; | ||
/** | ||
* Add ponyfill for AbortController | ||
*/ | ||
AbortController?: AbortControllerEsque | null; | ||
/** | ||
* Send all requests `as POST`s requests regardless of the procedure type | ||
* The HTTP handler must separately allow overriding the method. See: | ||
* @link https://trpc.io/docs/rpc | ||
* @see https://trpc.io/docs/rpc | ||
*/ | ||
@@ -51,3 +44,2 @@ methodOverride?: 'POST'; | ||
fetch?: FetchEsque; | ||
AbortController: AbortControllerEsque | null; | ||
transformer: CombinedDataTransformer; | ||
@@ -58,8 +50,7 @@ methodOverride?: 'POST'; | ||
export function resolveHTTPLinkOptions( | ||
opts: HTTPLinkBaseOptions<AnyRootTypes>, | ||
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), | ||
@@ -83,2 +74,3 @@ methodOverride: opts.methodOverride, | ||
mutation: 'POST', | ||
subscription: 'PATCH', | ||
} as const; | ||
@@ -98,3 +90,3 @@ | ||
function getInput(opts: GetInputOptions) { | ||
export function getInput(opts: GetInputOptions) { | ||
return 'input' in opts | ||
@@ -111,2 +103,3 @@ ? opts.transformer.input.serialize(opts.input) | ||
path: string; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -116,4 +109,5 @@ | ||
type GetBody = (opts: HTTPBaseRequestOptions) => RequestInitEsque['body']; | ||
export type ContentOptions = { | ||
batchModeHeader?: 'stream'; | ||
trpcAcceptHeader?: TRPCAcceptHeader; | ||
contentTypeHeader?: string; | ||
@@ -125,8 +119,15 @@ 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); | ||
@@ -155,3 +156,3 @@ if (input !== undefined && opts.methodOverride !== 'POST') { | ||
}, | ||
) => PromiseAndCancel<HTTPResult>; | ||
) => Promise<HTTPResult>; | ||
@@ -167,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); | ||
@@ -188,6 +220,2 @@ const body = opts.getBody(opts); | ||
})(); | ||
/* istanbul ignore if -- @preserve */ | ||
if (type === 'subscription') { | ||
throw new Error('Subscriptions should use wsLink'); | ||
} | ||
const headers = { | ||
@@ -197,5 +225,5 @@ ...(opts.contentTypeHeader | ||
: {}), | ||
...(opts.batchModeHeader | ||
? { 'trpc-batch-mode': opts.batchModeHeader } | ||
: {}), | ||
...(opts.trpcAcceptHeader | ||
? { 'trpc-accept': opts.trpcAcceptHeader } | ||
: undefined), | ||
...resolvedHeaders, | ||
@@ -206,3 +234,3 @@ }; | ||
method: opts.methodOverride ?? METHOD[type], | ||
signal: ac?.signal, | ||
signal: opts.signal, | ||
body, | ||
@@ -213,34 +241,18 @@ headers, | ||
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 }; | ||
} |
@@ -9,3 +9,6 @@ /// <reference lib="dom.iterable" /> | ||
import { observable, tap } from '@trpc/server/observable'; | ||
import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import'; | ||
import type { | ||
AnyRouter, | ||
InferrableClientTypes, | ||
} from '@trpc/server/unstable-core-do-not-import'; | ||
import type { TRPCClientError } from '../TRPCClientError'; | ||
@@ -19,6 +22,8 @@ import type { Operation, OperationResultEnvelope, TRPCLink } from './types'; | ||
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'; | ||
/** | ||
* @link https://trpc.io/docs/v11/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) |
import type { Observable, Observer } from '@trpc/server/observable'; | ||
import type { | ||
InferrableClientTypes, | ||
Maybe, | ||
TRPCResultMessage, | ||
@@ -9,7 +10,9 @@ TRPCSuccessResponse, | ||
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 +22,2 @@ /** | ||
*/ | ||
export type PromiseAndCancel<TValue> = { | ||
promise: Promise<TValue>; | ||
cancel: CancelFn; | ||
}; | ||
/** | ||
* @internal | ||
*/ | ||
export interface OperationContext extends Record<string, unknown> {} | ||
@@ -39,2 +34,3 @@ | ||
context: OperationContext; | ||
signal: Maybe<AbortSignal>; | ||
}; | ||
@@ -69,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; | ||
@@ -83,3 +80,6 @@ } | ||
TOutput, | ||
> = Observable<OperationResultEnvelope<TOutput>, TRPCClientError<TInferrable>>; | ||
> = Observable< | ||
OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, | ||
TRPCClientError<TInferrable> | ||
>; | ||
@@ -92,3 +92,6 @@ /** | ||
TOutput, | ||
> = Observer<OperationResultEnvelope<TOutput>, TRPCClientError<TInferrable>>; | ||
> = Observer< | ||
OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>, | ||
TRPCClientError<TInferrable> | ||
>; | ||
@@ -95,0 +98,0 @@ /** |
import type { Observer, UnsubscribeFn } from '@trpc/server/observable'; | ||
import { observable } from '@trpc/server/observable'; | ||
import { behaviorSubject, observable } from '@trpc/server/observable'; | ||
import type { TRPCConnectionParamsMessage } from '@trpc/server/rpc'; | ||
import type { | ||
@@ -7,3 +8,2 @@ AnyRouter, | ||
inferRouterError, | ||
MaybePromise, | ||
ProcedureType, | ||
@@ -20,2 +20,7 @@ TRPCClientIncomingMessage, | ||
import { getTransformer } from '../unstable-internals'; | ||
import type { TRPCConnectionState } from './internals/subscriptions'; | ||
import { | ||
resultOf, | ||
type UrlOptionsWithConnectionParams, | ||
} from './internals/urlWithConnectionParams'; | ||
import type { Operation, TRPCLink } from './types'; | ||
@@ -38,8 +43,4 @@ | ||
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 | ||
@@ -58,2 +59,6 @@ */ | ||
/** | ||
* Triggered when a WebSocket connection encounters an error | ||
*/ | ||
onError?: (evt?: Event) => void; | ||
/** | ||
* Triggered when a WebSocket connection is closed | ||
@@ -77,2 +82,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; | ||
}; | ||
} | ||
@@ -85,9 +109,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; | ||
@@ -113,3 +141,3 @@ const lazyOpts: LazyOptions = { | ||
type TCallbacks = WSCallbackObserver<AnyRouter, unknown>; | ||
type TRequest = { | ||
type WsRequest = { | ||
/** | ||
@@ -122,4 +150,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); | ||
@@ -152,2 +184,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); | ||
/** | ||
@@ -158,3 +205,3 @@ * tries to send the list of messages | ||
if (!activeConnection) { | ||
activeConnection = createConnection(); | ||
reconnect(null); | ||
return; | ||
@@ -185,3 +232,3 @@ } | ||
} | ||
function tryReconnect(conn: Connection) { | ||
function tryReconnect(cause: Error | null) { | ||
if (!!connectTimer) { | ||
@@ -191,5 +238,4 @@ return; | ||
conn.state = 'connecting'; | ||
const timeout = retryDelayFn(connectAttempt++); | ||
reconnectInMs(timeout); | ||
reconnectInMs(timeout, cause); | ||
} | ||
@@ -204,5 +250,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; | ||
@@ -212,9 +258,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); | ||
} | ||
@@ -228,7 +287,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, | ||
}); | ||
} | ||
@@ -247,5 +310,10 @@ | ||
if (!hasPendingRequests(activeConnection)) { | ||
if (!hasPendingRequests()) { | ||
activeConnection.ws?.close(); | ||
activeConnection = null; | ||
connectionState.next({ | ||
type: 'state', | ||
state: 'idle', | ||
error: null, | ||
}); | ||
} | ||
@@ -256,2 +324,4 @@ }, lazyOpts.closeMs); | ||
function createConnection(): Connection { | ||
let pingTimeout: ReturnType<typeof setTimeout> | undefined = undefined; | ||
let pongTimeout: ReturnType<typeof setTimeout> | undefined = undefined; | ||
const self: Connection = { | ||
@@ -264,11 +334,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; | ||
@@ -279,14 +399,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) => { | ||
@@ -298,3 +483,7 @@ if (self !== activeConnection) { | ||
if (req.method === 'reconnect') { | ||
reconnect(); | ||
reconnect( | ||
new TRPCWebSocketClosedError({ | ||
message: 'Server requested reconnect', | ||
}), | ||
); | ||
// notify subscribers | ||
@@ -317,10 +506,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' && | ||
@@ -332,3 +530,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(); | ||
@@ -347,46 +554,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; | ||
@@ -399,4 +590,6 @@ const envelope: TRPCRequestMessage = { | ||
path, | ||
lastEventId, | ||
}, | ||
}; | ||
pendingRequests[id] = { | ||
@@ -407,2 +600,3 @@ connection: null, | ||
op, | ||
lastEventId, | ||
}; | ||
@@ -431,2 +625,3 @@ | ||
} | ||
return { | ||
@@ -443,3 +638,5 @@ close: () => { | ||
TRPCClientError.from( | ||
new Error('Closed before connection was established'), | ||
new TRPCWebSocketClosedError({ | ||
message: 'Closed before connection was established', | ||
}), | ||
), | ||
@@ -449,3 +646,5 @@ ); | ||
} | ||
activeConnection && closeIfNoPending(activeConnection); | ||
if (activeConnection) { | ||
closeIfNoPending(activeConnection); | ||
} | ||
clearTimeout(connectTimer); | ||
@@ -459,6 +658,24 @@ 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>; | ||
/** | ||
* @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> = { | ||
@@ -468,4 +685,11 @@ client: TRPCWebSocketClient; | ||
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'; | ||
@@ -477,3 +701,6 @@ Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype); | ||
/** | ||
* @link https://trpc.io/docs/v11/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 | ||
*/ | ||
@@ -492,8 +719,19 @@ export function wsLink<TRouter extends AnyRouter>( | ||
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(); | ||
}, | ||
@@ -503,4 +741,4 @@ complete() { | ||
}, | ||
next(message) { | ||
const transformed = transformResult(message, transformer.output); | ||
next(event) { | ||
const transformed = transformResult(event, transformer.output); | ||
@@ -518,3 +756,3 @@ if (!transformed.ok) { | ||
unsub(); | ||
unsubscribeRequest(); | ||
observer.complete(); | ||
@@ -524,5 +762,7 @@ } | ||
}, | ||
); | ||
lastEventId: undefined, | ||
}); | ||
return () => { | ||
unsub(); | ||
unsubscribeRequest(); | ||
connState?.unsubscribe(); | ||
}; | ||
@@ -529,0 +769,0 @@ }); |
@@ -8,3 +8,2 @@ import type { | ||
import { | ||
getCauseFromUnknown, | ||
isObject, | ||
@@ -44,2 +43,12 @@ type DefaultErrorShape, | ||
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> | ||
@@ -86,3 +95,3 @@ extends Error | ||
public static from<TRouterOrProcedure extends InferrableClientTypes>( | ||
_cause: Error | TRPCErrorResponse<any>, | ||
_cause: Error | TRPCErrorResponse<any> | object, | ||
opts: { meta?: Record<string, unknown> } = {}, | ||
@@ -108,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), | ||
}); | ||
}, | ||
); | ||
} | ||
} |
export * from './internals/transformer'; | ||
export * from './links/internals/subscriptions'; |
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
Sorry, the diff of this file is not supported yet
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
307420
149
8026
1
2
11