@trpc/client
Advanced tools
| #!/usr/bin/env node | ||
| // Auto-generated by @tanstack/intent setup | ||
| // Exposes the intent end-user CLI for consumers of this library. | ||
| // Commit this file, then add to your package.json: | ||
| // "bin": { "intent": "./bin/intent.js" } | ||
| try { | ||
| await import('@tanstack/intent/intent-library'); | ||
| } catch (e) { | ||
| if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { | ||
| console.error('@tanstack/intent is not installed.'); | ||
| console.error(''); | ||
| console.error('Install it as a dev dependency:'); | ||
| console.error(' npm add -D @tanstack/intent'); | ||
| console.error(''); | ||
| console.error('Or run directly:'); | ||
| console.error(' npx @tanstack/intent@latest list'); | ||
| process.exit(1); | ||
| } | ||
| throw e; | ||
| } |
| import { __toESM, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs"; | ||
| import { TRPCClientError } from "./TRPCClientError-apv8gw59.mjs"; | ||
| import { getUrl, jsonHttpRequester, resolveHTTPLinkOptions } from "./httpUtils-BNq9QC3d.mjs"; | ||
| import { observable } from "@trpc/server/observable"; | ||
| import { transformResult } from "@trpc/server/unstable-core-do-not-import"; | ||
| //#region src/internals/dataLoader.ts | ||
| /** | ||
| * A function that should never be called unless we messed something up. | ||
| */ | ||
| const throwFatalError = () => { | ||
| throw new Error("Something went wrong. Please submit an issue at https://github.com/trpc/trpc/issues/new"); | ||
| }; | ||
| /** | ||
| * Dataloader that's very inspired by https://github.com/graphql/dataloader | ||
| * Less configuration, no caching, and allows you to cancel requests | ||
| * When cancelling a single fetch the whole batch will be cancelled only when _all_ items are cancelled | ||
| */ | ||
| function dataLoader(batchLoader) { | ||
| let pendingItems = null; | ||
| let dispatchTimer = null; | ||
| const destroyTimerAndPendingItems = () => { | ||
| clearTimeout(dispatchTimer); | ||
| dispatchTimer = null; | ||
| pendingItems = null; | ||
| }; | ||
| /** | ||
| * Iterate through the items and split them into groups based on the `batchLoader`'s validate function | ||
| */ | ||
| function groupItems(items) { | ||
| const groupedItems = [[]]; | ||
| let index = 0; | ||
| while (true) { | ||
| const item = items[index]; | ||
| if (!item) break; | ||
| const lastGroup = groupedItems[groupedItems.length - 1]; | ||
| if (item.aborted) { | ||
| var _item$reject; | ||
| (_item$reject = item.reject) === null || _item$reject === void 0 || _item$reject.call(item, new Error("Aborted")); | ||
| index++; | ||
| continue; | ||
| } | ||
| const isValid = batchLoader.validate(lastGroup.concat(item).map((it) => it.key)); | ||
| if (isValid) { | ||
| lastGroup.push(item); | ||
| index++; | ||
| continue; | ||
| } | ||
| if (lastGroup.length === 0) { | ||
| var _item$reject2; | ||
| (_item$reject2 = item.reject) === null || _item$reject2 === void 0 || _item$reject2.call(item, new Error("Input is too big for a single dispatch")); | ||
| index++; | ||
| continue; | ||
| } | ||
| groupedItems.push([]); | ||
| } | ||
| return groupedItems; | ||
| } | ||
| function dispatch() { | ||
| const groupedItems = groupItems(pendingItems); | ||
| destroyTimerAndPendingItems(); | ||
| for (const items of groupedItems) { | ||
| if (!items.length) continue; | ||
| const batch = { items }; | ||
| for (const item of items) item.batch = batch; | ||
| 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 { | ||
| var _item$resolve; | ||
| const value = await Promise.resolve(valueOrPromise); | ||
| (_item$resolve = item.resolve) === null || _item$resolve === void 0 || _item$resolve.call(item, value); | ||
| } catch (cause) { | ||
| var _item$reject3; | ||
| (_item$reject3 = item.reject) === null || _item$reject3 === void 0 || _item$reject3.call(item, cause); | ||
| } | ||
| item.batch = null; | ||
| item.reject = null; | ||
| item.resolve = null; | ||
| })); | ||
| for (const item of batch.items) { | ||
| var _item$reject4; | ||
| (_item$reject4 = item.reject) === null || _item$reject4 === void 0 || _item$reject4.call(item, new Error("Missing result")); | ||
| item.batch = null; | ||
| } | ||
| }).catch((cause) => { | ||
| for (const item of batch.items) { | ||
| var _item$reject5; | ||
| (_item$reject5 = item.reject) === null || _item$reject5 === void 0 || _item$reject5.call(item, cause); | ||
| item.batch = null; | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| function load(key) { | ||
| var _dispatchTimer; | ||
| const item = { | ||
| aborted: false, | ||
| key, | ||
| batch: null, | ||
| resolve: throwFatalError, | ||
| reject: throwFatalError | ||
| }; | ||
| const promise = new Promise((resolve, reject) => { | ||
| var _pendingItems; | ||
| item.reject = reject; | ||
| item.resolve = resolve; | ||
| (_pendingItems = pendingItems) !== null && _pendingItems !== void 0 || (pendingItems = []); | ||
| pendingItems.push(item); | ||
| }); | ||
| (_dispatchTimer = dispatchTimer) !== null && _dispatchTimer !== void 0 || (dispatchTimer = setTimeout(dispatch)); | ||
| return promise; | ||
| } | ||
| return { load }; | ||
| } | ||
| //#endregion | ||
| //#region src/internals/signals.ts | ||
| /** | ||
| * Like `Promise.all()` but for abort signals | ||
| * - When all signals have been aborted, the merged signal will be aborted | ||
| * - If one signal is `null`, no signal will be aborted | ||
| */ | ||
| function allAbortSignals(...signals) { | ||
| const ac = new AbortController(); | ||
| const count = signals.length; | ||
| let abortedCount = 0; | ||
| const onAbort = () => { | ||
| if (++abortedCount === count) ac.abort(); | ||
| }; | ||
| for (const signal of signals) if (signal === null || signal === void 0 ? void 0 : signal.aborted) onAbort(); | ||
| else signal === null || signal === void 0 || signal.addEventListener("abort", onAbort, { once: true }); | ||
| return ac.signal; | ||
| } | ||
| /** | ||
| * Like `Promise.race` but for abort signals | ||
| * | ||
| * Basically, a ponyfill for | ||
| * [`AbortSignal.any`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static). | ||
| */ | ||
| function raceAbortSignals(...signals) { | ||
| const ac = new AbortController(); | ||
| for (const signal of signals) if (signal === null || signal === void 0 ? void 0 : signal.aborted) ac.abort(); | ||
| else signal === null || signal === void 0 || signal.addEventListener("abort", () => ac.abort(), { once: true }); | ||
| return ac.signal; | ||
| } | ||
| function abortSignalToPromise(signal) { | ||
| return new Promise((_, reject) => { | ||
| if (signal.aborted) { | ||
| reject(signal.reason); | ||
| return; | ||
| } | ||
| signal.addEventListener("abort", () => { | ||
| reject(signal.reason); | ||
| }, { once: true }); | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/links/httpBatchLink.ts | ||
| var import_objectSpread2 = __toESM(require_objectSpread2(), 1); | ||
| /** | ||
| * @see https://trpc.io/docs/client/links/httpBatchLink | ||
| */ | ||
| function httpBatchLink(opts) { | ||
| var _opts$maxURLLength, _opts$maxItems; | ||
| const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
| const maxURLLength = (_opts$maxURLLength = opts.maxURLLength) !== null && _opts$maxURLLength !== void 0 ? _opts$maxURLLength : Infinity; | ||
| const maxItems = (_opts$maxItems = opts.maxItems) !== null && _opts$maxItems !== void 0 ? _opts$maxItems : Infinity; | ||
| return () => { | ||
| const batchLoader = (type) => { | ||
| return { | ||
| validate(batchOps) { | ||
| if (maxURLLength === Infinity && maxItems === Infinity) 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 = getUrl((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, 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((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, resolvedOpts), {}, { | ||
| path, | ||
| inputs, | ||
| type, | ||
| headers() { | ||
| if (!opts.headers) return {}; | ||
| if (typeof opts.headers === "function") return opts.headers({ opList: batchOps }); | ||
| return opts.headers; | ||
| }, | ||
| signal | ||
| })); | ||
| const resJSON = Array.isArray(res.json) ? res.json : batchOps.map(() => res.json); | ||
| const result = resJSON.map((item) => ({ | ||
| meta: res.meta, | ||
| json: item | ||
| })); | ||
| return result; | ||
| } | ||
| }; | ||
| }; | ||
| const query = dataLoader(batchLoader("query")); | ||
| const mutation = dataLoader(batchLoader("mutation")); | ||
| const loaders = { | ||
| query, | ||
| mutation | ||
| }; | ||
| return ({ op }) => { | ||
| return observable((observer) => { | ||
| /* istanbul ignore if -- @preserve */ | ||
| if (op.type === "subscription") throw new Error("Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`"); | ||
| const loader = loaders[op.type]; | ||
| const promise = loader.load(op); | ||
| let _res = void 0; | ||
| promise.then((res) => { | ||
| _res = res; | ||
| const transformed = transformResult(res.json, resolvedOpts.transformer.output); | ||
| 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 === null || _res === void 0 ? void 0 : _res.meta })); | ||
| }); | ||
| return () => {}; | ||
| }); | ||
| }; | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { abortSignalToPromise, allAbortSignals, dataLoader, httpBatchLink, raceAbortSignals }; | ||
| //# sourceMappingURL=httpBatchLink-CaWjh1oW.mjs.map |
| {"version":3,"file":"httpBatchLink-CaWjh1oW.mjs","names":["batchLoader: BatchLoader<TKey, TValue>","pendingItems: BatchItem<TKey, TValue>[] | null","dispatchTimer: ReturnType<typeof setTimeout> | null","items: BatchItem<TKey, TValue>[]","groupedItems: BatchItem<TKey, TValue>[][]","batch: Batch<TKey, TValue>","key: TKey","item: BatchItem<TKey, TValue>","signal: AbortSignal","opts: HTTPBatchLinkOptions<TRouter['_def']['_config']['$types']>","type: ProcedureType"],"sources":["../src/internals/dataLoader.ts","../src/internals/signals.ts","../src/links/httpBatchLink.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-non-null-assertion */\n\ntype BatchItem<TKey, TValue> = {\n aborted: boolean;\n key: TKey;\n resolve: ((value: TValue) => void) | null;\n reject: ((error: Error) => void) | null;\n batch: Batch<TKey, TValue> | null;\n};\ntype Batch<TKey, TValue> = {\n items: BatchItem<TKey, TValue>[];\n};\nexport type BatchLoader<TKey, TValue> = {\n validate: (keys: TKey[]) => boolean;\n fetch: (keys: TKey[]) => Promise<TValue[] | Promise<TValue>[]>;\n};\n\n/**\n * A function that should never be called unless we messed something up.\n */\nconst throwFatalError = () => {\n throw new Error(\n 'Something went wrong. Please submit an issue at https://github.com/trpc/trpc/issues/new',\n );\n};\n\n/**\n * Dataloader that's very inspired by https://github.com/graphql/dataloader\n * Less configuration, no caching, and allows you to cancel requests\n * When cancelling a single fetch the whole batch will be cancelled only when _all_ items are cancelled\n */\nexport function dataLoader<TKey, TValue>(\n batchLoader: BatchLoader<TKey, TValue>,\n) {\n let pendingItems: BatchItem<TKey, TValue>[] | null = null;\n let dispatchTimer: ReturnType<typeof setTimeout> | null = null;\n\n const destroyTimerAndPendingItems = () => {\n clearTimeout(dispatchTimer as any);\n dispatchTimer = null;\n pendingItems = null;\n };\n\n /**\n * Iterate through the items and split them into groups based on the `batchLoader`'s validate function\n */\n function groupItems(items: BatchItem<TKey, TValue>[]) {\n const groupedItems: BatchItem<TKey, TValue>[][] = [[]];\n let index = 0;\n while (true) {\n const item = items[index];\n if (!item) {\n // we're done\n break;\n }\n const lastGroup = groupedItems[groupedItems.length - 1]!;\n\n if (item.aborted) {\n // Item was aborted before it was dispatched\n item.reject?.(new Error('Aborted'));\n index++;\n continue;\n }\n\n const isValid = batchLoader.validate(\n lastGroup.concat(item).map((it) => it.key),\n );\n\n if (isValid) {\n lastGroup.push(item);\n index++;\n continue;\n }\n\n if (lastGroup.length === 0) {\n item.reject?.(new Error('Input is too big for a single dispatch'));\n index++;\n continue;\n }\n // Create new group, next iteration will try to add the item to that\n groupedItems.push([]);\n }\n return groupedItems;\n }\n\n function dispatch() {\n const groupedItems = groupItems(pendingItems!);\n destroyTimerAndPendingItems();\n\n // Create batches for each group of items\n for (const items of groupedItems) {\n if (!items.length) {\n continue;\n }\n const batch: Batch<TKey, TValue> = {\n items,\n };\n for (const item of items) {\n item.batch = batch;\n }\n const promise = batchLoader.fetch(batch.items.map((_item) => _item.key));\n\n promise\n .then(async (result) => {\n await Promise.all(\n result.map(async (valueOrPromise, index) => {\n const item = batch.items[index]!;\n try {\n const value = await Promise.resolve(valueOrPromise);\n\n item.resolve?.(value);\n } catch (cause) {\n item.reject?.(cause as Error);\n }\n\n item.batch = null;\n item.reject = null;\n item.resolve = null;\n }),\n );\n\n for (const item of batch.items) {\n item.reject?.(new Error('Missing result'));\n item.batch = null;\n }\n })\n .catch((cause) => {\n for (const item of batch.items) {\n item.reject?.(cause);\n item.batch = null;\n }\n });\n }\n }\n function load(key: TKey): Promise<TValue> {\n const item: BatchItem<TKey, TValue> = {\n aborted: false,\n key,\n batch: null,\n resolve: throwFatalError,\n reject: throwFatalError,\n };\n\n const promise = new Promise<TValue>((resolve, reject) => {\n item.reject = reject;\n item.resolve = resolve;\n\n pendingItems ??= [];\n pendingItems.push(item);\n });\n\n dispatchTimer ??= setTimeout(dispatch);\n\n return promise;\n }\n\n return {\n load,\n };\n}\n","import type { Maybe } from '@trpc/server/unstable-core-do-not-import';\n\n/**\n * Like `Promise.all()` but for abort signals\n * - When all signals have been aborted, the merged signal will be aborted\n * - If one signal is `null`, no signal will be aborted\n */\nexport function allAbortSignals(...signals: Maybe<AbortSignal>[]): AbortSignal {\n const ac = new AbortController();\n\n const count = signals.length;\n\n let abortedCount = 0;\n\n const onAbort = () => {\n if (++abortedCount === count) {\n ac.abort();\n }\n };\n\n for (const signal of signals) {\n if (signal?.aborted) {\n onAbort();\n } else {\n signal?.addEventListener('abort', onAbort, {\n once: true,\n });\n }\n }\n\n return ac.signal;\n}\n\n/**\n * Like `Promise.race` but for abort signals\n *\n * Basically, a ponyfill for\n * [`AbortSignal.any`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static).\n */\nexport function raceAbortSignals(\n ...signals: Maybe<AbortSignal>[]\n): AbortSignal {\n const ac = new AbortController();\n\n for (const signal of signals) {\n if (signal?.aborted) {\n ac.abort();\n } else {\n signal?.addEventListener('abort', () => ac.abort(), { once: true });\n }\n }\n\n return ac.signal;\n}\n\nexport function abortSignalToPromise(signal: AbortSignal): Promise<never> {\n return new Promise((_, reject) => {\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n signal.addEventListener(\n 'abort',\n () => {\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n","import type { AnyRouter, ProcedureType } from '@trpc/server';\nimport { observable } from '@trpc/server/observable';\nimport { transformResult } from '@trpc/server/unstable-core-do-not-import';\nimport type { BatchLoader } from '../internals/dataLoader';\nimport { dataLoader } from '../internals/dataLoader';\nimport { allAbortSignals } from '../internals/signals';\nimport type { NonEmptyArray } from '../internals/types';\nimport { TRPCClientError } from '../TRPCClientError';\nimport type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions';\nimport type { HTTPResult } from './internals/httpUtils';\nimport {\n getUrl,\n jsonHttpRequester,\n resolveHTTPLinkOptions,\n} from './internals/httpUtils';\nimport type { Operation, TRPCLink } from './types';\n\n/**\n * @see https://trpc.io/docs/client/links/httpBatchLink\n */\nexport function httpBatchLink<TRouter extends AnyRouter>(\n opts: HTTPBatchLinkOptions<TRouter['_def']['_config']['$types']>,\n): TRPCLink<TRouter> {\n const resolvedOpts = resolveHTTPLinkOptions(opts);\n const maxURLLength = opts.maxURLLength ?? Infinity;\n const maxItems = opts.maxItems ?? Infinity;\n\n return () => {\n const batchLoader = (\n type: ProcedureType,\n ): BatchLoader<Operation, HTTPResult> => {\n return {\n validate(batchOps) {\n if (maxURLLength === Infinity && maxItems === Infinity) {\n // escape hatch for quick calcs\n return true;\n }\n if (batchOps.length > maxItems) {\n return false;\n }\n const path = batchOps.map((op) => op.path).join(',');\n const inputs = batchOps.map((op) => op.input);\n\n const url = getUrl({\n ...resolvedOpts,\n type,\n path,\n inputs,\n signal: null,\n });\n\n return url.length <= maxURLLength;\n },\n async fetch(batchOps) {\n const path = batchOps.map((op) => op.path).join(',');\n const inputs = batchOps.map((op) => op.input);\n const signal = allAbortSignals(...batchOps.map((op) => op.signal));\n\n const res = await jsonHttpRequester({\n ...resolvedOpts,\n path,\n inputs,\n type,\n headers() {\n if (!opts.headers) {\n return {};\n }\n if (typeof opts.headers === 'function') {\n return opts.headers({\n opList: batchOps as NonEmptyArray<Operation>,\n });\n }\n return opts.headers;\n },\n signal,\n });\n const resJSON = Array.isArray(res.json)\n ? res.json\n : batchOps.map(() => res.json);\n const result = resJSON.map((item) => ({\n meta: res.meta,\n json: item,\n }));\n return result;\n },\n };\n };\n\n const query = dataLoader(batchLoader('query'));\n const mutation = dataLoader(batchLoader('mutation'));\n\n const loaders = { query, mutation };\n return ({ op }) => {\n return observable((observer) => {\n /* istanbul ignore if -- @preserve */\n if (op.type === 'subscription') {\n throw new Error(\n 'Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`',\n );\n }\n const loader = loaders[op.type];\n const promise = loader.load(op);\n\n let _res = undefined as HTTPResult | undefined;\n promise\n .then((res) => {\n _res = res;\n const transformed = transformResult(\n res.json,\n resolvedOpts.transformer.output,\n );\n\n if (!transformed.ok) {\n observer.error(\n TRPCClientError.from(transformed.error, {\n meta: res.meta,\n }),\n );\n return;\n }\n observer.next({\n context: res.meta,\n result: transformed.result,\n });\n observer.complete();\n })\n .catch((err) => {\n observer.error(\n TRPCClientError.from(err, {\n meta: _res?.meta,\n }),\n );\n });\n\n return () => {\n // noop\n };\n });\n };\n };\n}\n"],"mappings":"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"} |
| import { __toESM, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs"; | ||
| import { TRPCClientError } from "./TRPCClientError-apv8gw59.mjs"; | ||
| import { getUrl, httpRequest, jsonHttpRequester, resolveHTTPLinkOptions } from "./httpUtils-BNq9QC3d.mjs"; | ||
| import { observable } from "@trpc/server/observable"; | ||
| import { transformResult } from "@trpc/server/unstable-core-do-not-import"; | ||
| //#region src/links/internals/contentTypes.ts | ||
| function isOctetType(input) { | ||
| return input instanceof Uint8Array || input instanceof Blob; | ||
| } | ||
| function isFormData(input) { | ||
| return input instanceof FormData; | ||
| } | ||
| function isNonJsonSerializable(input) { | ||
| return isOctetType(input) || isFormData(input); | ||
| } | ||
| //#endregion | ||
| //#region src/links/httpLink.ts | ||
| var import_objectSpread2 = __toESM(require_objectSpread2(), 1); | ||
| const universalRequester = (opts) => { | ||
| if ("input" in opts) { | ||
| const { input } = opts; | ||
| if (isFormData(input)) { | ||
| if (opts.type !== "mutation" && opts.methodOverride !== "POST") throw new Error("FormData is only supported for mutations"); | ||
| return httpRequest((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts), {}, { | ||
| contentTypeHeader: void 0, | ||
| 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((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts), {}, { | ||
| contentTypeHeader: "application/octet-stream", | ||
| getUrl, | ||
| getBody: () => input | ||
| })); | ||
| } | ||
| } | ||
| return jsonHttpRequester(opts); | ||
| }; | ||
| /** | ||
| * @see https://trpc.io/docs/client/links/httpLink | ||
| */ | ||
| function httpLink(opts) { | ||
| const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
| return () => { | ||
| return (operationOpts) => { | ||
| const { op } = operationOpts; | ||
| 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((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, resolvedOpts), {}, { | ||
| type, | ||
| path, | ||
| input, | ||
| signal: op.signal, | ||
| headers() { | ||
| if (!opts.headers) return {}; | ||
| if (typeof opts.headers === "function") return opts.headers({ op }); | ||
| return opts.headers; | ||
| } | ||
| })); | ||
| let meta = void 0; | ||
| 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 () => {}; | ||
| }); | ||
| }; | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { httpLink, isFormData, isNonJsonSerializable, isOctetType }; | ||
| //# sourceMappingURL=httpLink-oiU8eqFi.mjs.map |
| {"version":3,"file":"httpLink-oiU8eqFi.mjs","names":["input: unknown","universalRequester: Requester","opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>","meta: HTTPResult['meta'] | undefined"],"sources":["../src/links/internals/contentTypes.ts","../src/links/httpLink.ts"],"sourcesContent":["export function isOctetType(\n input: unknown,\n): input is Uint8Array<ArrayBuffer> | Blob {\n return (\n input instanceof Uint8Array ||\n // File extends from Blob but is only available in nodejs from v20\n input instanceof Blob\n );\n}\n\nexport function isFormData(input: unknown) {\n return input instanceof FormData;\n}\n\nexport function isNonJsonSerializable(input: unknown) {\n return isOctetType(input) || isFormData(input);\n}\n","import { observable } from '@trpc/server/observable';\nimport type {\n AnyClientTypes,\n AnyRouter,\n} from '@trpc/server/unstable-core-do-not-import';\nimport { transformResult } from '@trpc/server/unstable-core-do-not-import';\nimport { TRPCClientError } from '../TRPCClientError';\nimport type {\n HTTPLinkBaseOptions,\n HTTPResult,\n Requester,\n} from './internals/httpUtils';\nimport {\n getUrl,\n httpRequest,\n jsonHttpRequester,\n resolveHTTPLinkOptions,\n} from './internals/httpUtils';\nimport {\n isFormData,\n isOctetType,\n type HTTPHeaders,\n type Operation,\n type TRPCLink,\n} from './types';\n\nexport type HTTPLinkOptions<TRoot extends AnyClientTypes> =\n HTTPLinkBaseOptions<TRoot> & {\n /**\n * Headers to be set on outgoing requests or a callback that of said headers\n * @see http://trpc.io/docs/client/headers\n */\n headers?:\n | HTTPHeaders\n | ((opts: { op: Operation }) => HTTPHeaders | Promise<HTTPHeaders>);\n };\n\nconst universalRequester: Requester = (opts) => {\n if ('input' in opts) {\n const { input } = opts;\n if (isFormData(input)) {\n if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') {\n throw new Error('FormData is only supported for mutations');\n }\n\n return httpRequest({\n ...opts,\n // The browser will set this automatically and include the boundary= in it\n contentTypeHeader: undefined,\n getUrl,\n getBody: () => input,\n });\n }\n\n if (isOctetType(input)) {\n if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') {\n throw new Error('Octet type input is only supported for mutations');\n }\n\n return httpRequest({\n ...opts,\n contentTypeHeader: 'application/octet-stream',\n getUrl,\n getBody: () => input,\n });\n }\n }\n\n return jsonHttpRequester(opts);\n};\n\n/**\n * @see https://trpc.io/docs/client/links/httpLink\n */\nexport function httpLink<TRouter extends AnyRouter = AnyRouter>(\n opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>,\n): TRPCLink<TRouter> {\n const resolvedOpts = resolveHTTPLinkOptions(opts);\n return () => {\n return (operationOpts) => {\n const { op } = operationOpts;\n return observable((observer) => {\n const { path, input, type } = op;\n /* istanbul ignore if -- @preserve */\n if (type === 'subscription') {\n throw new Error(\n 'Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`',\n );\n }\n\n const request = universalRequester({\n ...resolvedOpts,\n type,\n path,\n input,\n signal: op.signal,\n headers() {\n if (!opts.headers) {\n return {};\n }\n if (typeof opts.headers === 'function') {\n return opts.headers({\n op,\n });\n }\n return opts.headers;\n },\n });\n let meta: HTTPResult['meta'] | undefined = undefined;\n request\n .then((res) => {\n meta = res.meta;\n const transformed = transformResult(\n res.json,\n resolvedOpts.transformer.output,\n );\n\n if (!transformed.ok) {\n observer.error(\n TRPCClientError.from(transformed.error, {\n meta,\n }),\n );\n return;\n }\n observer.next({\n context: res.meta,\n result: transformed.result,\n });\n observer.complete();\n })\n .catch((cause) => {\n observer.error(TRPCClientError.from(cause, { meta }));\n });\n\n return () => {\n // noop\n };\n });\n };\n };\n}\n"],"mappings":";;;;;;;AAAA,SAAgB,YACdA,OACyC;AACzC,QACE,iBAAiB,cAEjB,iBAAiB;AAEpB;AAED,SAAgB,WAAWA,OAAgB;AACzC,QAAO,iBAAiB;AACzB;AAED,SAAgB,sBAAsBA,OAAgB;AACpD,QAAO,YAAY,MAAM,IAAI,WAAW,MAAM;AAC/C;;;;;ACqBD,MAAMC,qBAAgC,CAAC,SAAS;AAC9C,KAAI,WAAW,MAAM;EACnB,MAAM,EAAE,OAAO,GAAG;AAClB,MAAI,WAAW,MAAM,EAAE;AACrB,OAAI,KAAK,SAAS,cAAc,KAAK,mBAAmB,OACtD,OAAM,IAAI,MAAM;AAGlB,UAAO,oFACF;IAEH;IACA;IACA,SAAS,MAAM;MACf;EACH;AAED,MAAI,YAAY,MAAM,EAAE;AACtB,OAAI,KAAK,SAAS,cAAc,KAAK,mBAAmB,OACtD,OAAM,IAAI,MAAM;AAGlB,UAAO,oFACF;IACH,mBAAmB;IACnB;IACA,SAAS,MAAM;MACf;EACH;CACF;AAED,QAAO,kBAAkB,KAAK;AAC/B;;;;AAKD,SAAgB,SACdC,MACmB;CACnB,MAAM,eAAe,uBAAuB,KAAK;AACjD,QAAO,MAAM;AACX,SAAO,CAAC,kBAAkB;GACxB,MAAM,EAAE,IAAI,GAAG;AACf,UAAO,WAAW,CAAC,aAAa;IAC9B,MAAM,EAAE,MAAM,OAAO,MAAM,GAAG;;AAE9B,QAAI,SAAS,eACX,OAAM,IAAI,MACR;IAIJ,MAAM,UAAU,2FACX;KACH;KACA;KACA;KACA,QAAQ,GAAG;KACX,UAAU;AACR,WAAK,KAAK,QACR,QAAO,CAAE;AAEX,iBAAW,KAAK,YAAY,WAC1B,QAAO,KAAK,QAAQ,EAClB,GACD,EAAC;AAEJ,aAAO,KAAK;KACb;OACD;IACF,IAAIC;AACJ,YACG,KAAK,CAAC,QAAQ;AACb,YAAO,IAAI;KACX,MAAM,cAAc,gBAClB,IAAI,MACJ,aAAa,YAAY,OAC1B;AAED,UAAK,YAAY,IAAI;AACnB,eAAS,MACP,gBAAgB,KAAK,YAAY,OAAO,EACtC,KACD,EAAC,CACH;AACD;KACD;AACD,cAAS,KAAK;MACZ,SAAS,IAAI;MACb,QAAQ,YAAY;KACrB,EAAC;AACF,cAAS,UAAU;IACpB,EAAC,CACD,MAAM,CAAC,UAAU;AAChB,cAAS,MAAM,gBAAgB,KAAK,OAAO,EAAE,KAAM,EAAC,CAAC;IACtD,EAAC;AAEJ,WAAO,MAAM,CAEZ;GACF,EAAC;EACH;CACF;AACF"} |
| import { __toESM, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs"; | ||
| import { getTransformer } from "./unstable-internals-Bg7n9BBj.mjs"; | ||
| //#region src/getFetch.ts | ||
| const isFunction = (fn) => typeof fn === "function"; | ||
| function getFetch(customFetchImpl) { | ||
| if (customFetchImpl) return customFetchImpl; | ||
| if (typeof window !== "undefined" && isFunction(window.fetch)) return window.fetch; | ||
| if (typeof globalThis !== "undefined" && isFunction(globalThis.fetch)) return globalThis.fetch; | ||
| throw new Error("No fetch implementation found"); | ||
| } | ||
| //#endregion | ||
| //#region src/links/internals/httpUtils.ts | ||
| var import_objectSpread2 = __toESM(require_objectSpread2()); | ||
| function resolveHTTPLinkOptions(opts) { | ||
| return { | ||
| url: opts.url.toString(), | ||
| fetch: opts.fetch, | ||
| transformer: getTransformer(opts.transformer), | ||
| methodOverride: opts.methodOverride | ||
| }; | ||
| } | ||
| function arrayToDict(array) { | ||
| const dict = {}; | ||
| for (let index = 0; index < array.length; index++) { | ||
| const element = array[index]; | ||
| dict[index] = element; | ||
| } | ||
| return dict; | ||
| } | ||
| const METHOD = { | ||
| query: "GET", | ||
| mutation: "POST", | ||
| subscription: "PATCH" | ||
| }; | ||
| function getInput(opts) { | ||
| return "input" in opts ? opts.transformer.input.serialize(opts.input) : arrayToDict(opts.inputs.map((_input) => opts.transformer.input.serialize(_input))); | ||
| } | ||
| const getUrl = (opts) => { | ||
| const parts = opts.url.split("?"); | ||
| const base = parts[0].replace(/\/$/, ""); | ||
| 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" || opts.type === "subscription") { | ||
| const input = getInput(opts); | ||
| if (input !== void 0 && opts.methodOverride !== "POST") queryParts.push(`input=${encodeURIComponent(JSON.stringify(input))}`); | ||
| } | ||
| if (queryParts.length) url += "?" + queryParts.join("&"); | ||
| return url; | ||
| }; | ||
| const getBody = (opts) => { | ||
| if (opts.type === "query" && opts.methodOverride !== "POST") return void 0; | ||
| const input = getInput(opts); | ||
| return input !== void 0 ? JSON.stringify(input) : void 0; | ||
| }; | ||
| const jsonHttpRequester = (opts) => { | ||
| return httpRequest((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts), {}, { | ||
| contentTypeHeader: "application/json", | ||
| getUrl, | ||
| getBody | ||
| })); | ||
| }; | ||
| /** | ||
| * Polyfill for DOMException with AbortError name | ||
| */ | ||
| var AbortError = class 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) => { | ||
| var _signal$throwIfAborte; | ||
| if (!(signal === null || signal === void 0 ? void 0 : signal.aborted)) return; | ||
| (_signal$throwIfAborte = signal.throwIfAborted) === null || _signal$throwIfAborte === void 0 || _signal$throwIfAborte.call(signal); | ||
| if (typeof DOMException !== "undefined") throw new DOMException("AbortError", "AbortError"); | ||
| throw new AbortError(); | ||
| }; | ||
| async function fetchHTTPResponse(opts) { | ||
| var _opts$methodOverride, _opts$trpcAcceptHeade; | ||
| throwIfAborted(opts.signal); | ||
| const url = opts.getUrl(opts); | ||
| const body = opts.getBody(opts); | ||
| const method = (_opts$methodOverride = opts.methodOverride) !== null && _opts$methodOverride !== void 0 ? _opts$methodOverride : METHOD[opts.type]; | ||
| const resolvedHeaders = await (async () => { | ||
| const heads = await opts.headers(); | ||
| if (Symbol.iterator in heads) return Object.fromEntries(heads); | ||
| return heads; | ||
| })(); | ||
| const headers = (0, import_objectSpread2.default)((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts.contentTypeHeader && method !== "GET" ? { "content-type": opts.contentTypeHeader } : {}), opts.trpcAcceptHeader ? { [(_opts$trpcAcceptHeade = opts.trpcAcceptHeaderKey) !== null && _opts$trpcAcceptHeade !== void 0 ? _opts$trpcAcceptHeade : "trpc-accept"]: opts.trpcAcceptHeader } : void 0), resolvedHeaders); | ||
| return getFetch(opts.fetch)(url, { | ||
| method, | ||
| signal: opts.signal, | ||
| body, | ||
| headers | ||
| }); | ||
| } | ||
| async function httpRequest(opts) { | ||
| const meta = {}; | ||
| const res = await fetchHTTPResponse(opts); | ||
| meta.response = res; | ||
| const json = await res.json(); | ||
| meta.responseJSON = json; | ||
| return { | ||
| json, | ||
| meta | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { fetchHTTPResponse, getBody, getFetch, getUrl, httpRequest, jsonHttpRequester, resolveHTTPLinkOptions }; | ||
| //# sourceMappingURL=httpUtils-BNq9QC3d.mjs.map |
| {"version":3,"file":"httpUtils-BNq9QC3d.mjs","names":["fn: unknown","customFetchImpl?: FetchEsque | NativeFetchEsque","opts: HTTPLinkBaseOptions<AnyClientTypes>","array: unknown[]","dict: Record<number, unknown>","opts: GetInputOptions","getUrl: GetUrl","queryParts: string[]","getBody: GetBody","jsonHttpRequester: Requester","signal: Maybe<AbortSignal>","opts: HTTPRequestOptions"],"sources":["../src/getFetch.ts","../src/links/internals/httpUtils.ts"],"sourcesContent":["import type { FetchEsque, NativeFetchEsque } from './internals/types';\n\ntype AnyFn = (...args: any[]) => unknown;\n\nconst isFunction = (fn: unknown): fn is AnyFn => typeof fn === 'function';\n\nexport function getFetch(\n customFetchImpl?: FetchEsque | NativeFetchEsque,\n): FetchEsque {\n if (customFetchImpl) {\n return customFetchImpl as FetchEsque;\n }\n\n if (typeof window !== 'undefined' && isFunction(window.fetch)) {\n return window.fetch as FetchEsque;\n }\n\n if (typeof globalThis !== 'undefined' && isFunction(globalThis.fetch)) {\n return globalThis.fetch as FetchEsque;\n }\n\n throw new Error('No fetch implementation found');\n}\n","import type {\n AnyClientTypes,\n CombinedDataTransformer,\n Maybe,\n ProcedureType,\n TRPCAcceptHeader,\n TRPCResponse,\n} from '@trpc/server/unstable-core-do-not-import';\nimport { getFetch } from '../../getFetch';\nimport type {\n FetchEsque,\n RequestInitEsque,\n ResponseEsque,\n} from '../../internals/types';\nimport type { TransformerOptions } from '../../unstable-internals';\nimport { getTransformer } from '../../unstable-internals';\nimport type { HTTPHeaders } from '../types';\n\n/**\n * @internal\n */\nexport type HTTPLinkBaseOptions<\n TRoot extends Pick<AnyClientTypes, 'transformer'>,\n> = {\n url: string | URL;\n /**\n * Add ponyfill for fetch\n */\n fetch?: FetchEsque;\n /**\n * Send all requests `as POST`s requests regardless of the procedure type\n * The HTTP handler must separately allow overriding the method. See:\n * @see https://trpc.io/docs/rpc\n */\n methodOverride?: 'POST';\n} & TransformerOptions<TRoot>;\n\nexport interface ResolvedHTTPLinkOptions {\n url: string;\n fetch?: FetchEsque;\n transformer: CombinedDataTransformer;\n methodOverride?: 'POST';\n}\n\nexport function resolveHTTPLinkOptions(\n opts: HTTPLinkBaseOptions<AnyClientTypes>,\n): ResolvedHTTPLinkOptions {\n return {\n url: opts.url.toString(),\n fetch: opts.fetch,\n transformer: getTransformer(opts.transformer),\n methodOverride: opts.methodOverride,\n };\n}\n\n// https://github.com/trpc/trpc/pull/669\nfunction arrayToDict(array: unknown[]) {\n const dict: Record<number, unknown> = {};\n for (let index = 0; index < array.length; index++) {\n const element = array[index];\n dict[index] = element;\n }\n return dict;\n}\n\nconst METHOD = {\n query: 'GET',\n mutation: 'POST',\n subscription: 'PATCH',\n} as const;\n\nexport interface HTTPResult {\n json: TRPCResponse;\n meta: {\n response: ResponseEsque;\n responseJSON?: unknown;\n };\n}\n\ntype GetInputOptions = {\n transformer: CombinedDataTransformer;\n} & ({ input: unknown } | { inputs: unknown[] });\n\nexport function getInput(opts: GetInputOptions) {\n return 'input' in opts\n ? opts.transformer.input.serialize(opts.input)\n : arrayToDict(\n opts.inputs.map((_input) => opts.transformer.input.serialize(_input)),\n );\n}\n\nexport type HTTPBaseRequestOptions = GetInputOptions &\n ResolvedHTTPLinkOptions & {\n type: ProcedureType;\n path: string;\n signal: Maybe<AbortSignal>;\n };\n\ntype GetUrl = (opts: HTTPBaseRequestOptions) => string;\ntype GetBody = (opts: HTTPBaseRequestOptions) => RequestInitEsque['body'];\n\nexport type ContentOptions = {\n trpcAcceptHeader?: TRPCAcceptHeader;\n trpcAcceptHeaderKey?: 'trpc-accept' | 'accept';\n contentTypeHeader?: string;\n getUrl: GetUrl;\n getBody: GetBody;\n};\n\nexport const getUrl: GetUrl = (opts) => {\n const parts = opts.url.split('?') as [string, string?];\n const base = parts[0].replace(/\\/$/, ''); // Remove any trailing slashes\n\n let url = base + '/' + opts.path;\n const queryParts: string[] = [];\n\n if (parts[1]) {\n queryParts.push(parts[1]);\n }\n if ('inputs' in opts) {\n queryParts.push('batch=1');\n }\n if (opts.type === 'query' || opts.type === 'subscription') {\n const input = getInput(opts);\n if (input !== undefined && opts.methodOverride !== 'POST') {\n queryParts.push(`input=${encodeURIComponent(JSON.stringify(input))}`);\n }\n }\n if (queryParts.length) {\n url += '?' + queryParts.join('&');\n }\n return url;\n};\n\nexport const getBody: GetBody = (opts) => {\n if (opts.type === 'query' && opts.methodOverride !== 'POST') {\n return undefined;\n }\n const input = getInput(opts);\n return input !== undefined ? JSON.stringify(input) : undefined;\n};\n\nexport type Requester = (\n opts: HTTPBaseRequestOptions & {\n headers: () => HTTPHeaders | Promise<HTTPHeaders>;\n },\n) => Promise<HTTPResult>;\n\nexport const jsonHttpRequester: Requester = (opts) => {\n return httpRequest({\n ...opts,\n contentTypeHeader: 'application/json',\n getUrl,\n getBody,\n });\n};\n\n/**\n * Polyfill for DOMException with AbortError name\n */\nclass AbortError extends Error {\n constructor() {\n const name = 'AbortError';\n super(name);\n this.name = name;\n this.message = name;\n }\n}\n\nexport type HTTPRequestOptions = ContentOptions &\n HTTPBaseRequestOptions & {\n headers: () => HTTPHeaders | Promise<HTTPHeaders>;\n };\n\n/**\n * Polyfill for `signal.throwIfAborted()`\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted\n */\nconst throwIfAborted = (signal: Maybe<AbortSignal>) => {\n if (!signal?.aborted) {\n return;\n }\n // If available, use the native implementation\n signal.throwIfAborted?.();\n\n // If we have `DOMException`, use it\n if (typeof DOMException !== 'undefined') {\n throw new DOMException('AbortError', 'AbortError');\n }\n\n // Otherwise, use our own implementation\n throw new AbortError();\n};\n\nexport async function fetchHTTPResponse(opts: HTTPRequestOptions) {\n throwIfAborted(opts.signal);\n\n const url = opts.getUrl(opts);\n const body = opts.getBody(opts);\n const method = opts.methodOverride ?? METHOD[opts.type];\n const resolvedHeaders = await (async () => {\n const heads = await opts.headers();\n if (Symbol.iterator in heads) {\n return Object.fromEntries(heads);\n }\n return heads;\n })();\n const headers = {\n ...(opts.contentTypeHeader && method !== 'GET'\n ? { 'content-type': opts.contentTypeHeader }\n : {}),\n ...(opts.trpcAcceptHeader\n ? { [opts.trpcAcceptHeaderKey ?? 'trpc-accept']: opts.trpcAcceptHeader }\n : undefined),\n ...resolvedHeaders,\n };\n\n return getFetch(opts.fetch)(url, {\n method,\n signal: opts.signal,\n body,\n headers,\n });\n}\n\nexport async function httpRequest(\n opts: HTTPRequestOptions,\n): Promise<HTTPResult> {\n const meta = {} as HTTPResult['meta'];\n\n const res = await fetchHTTPResponse(opts);\n meta.response = res;\n\n const json = await res.json();\n\n meta.responseJSON = json;\n\n return {\n json: json as TRPCResponse,\n meta,\n };\n}\n"],"mappings":"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"} |
| --- | ||
| name: client-setup | ||
| description: > | ||
| Create a vanilla tRPC client with createTRPCClient<AppRouter>(), configure | ||
| link chain with httpBatchLink/httpLink, dynamic headers for auth, transformer | ||
| on links (not client constructor). Infer types with inferRouterInputs and | ||
| inferRouterOutputs. AbortController signal support. TRPCClientError typing. | ||
| type: core | ||
| library: trpc | ||
| library_version: '11.14.0' | ||
| requires: | ||
| - server-setup | ||
| sources: | ||
| - www/docs/client/overview.md | ||
| - www/docs/client/vanilla/overview.md | ||
| - www/docs/client/vanilla/setup.mdx | ||
| - www/docs/client/vanilla/infer-types.md | ||
| - www/docs/client/headers.md | ||
| - packages/client/src/internals/TRPCUntypedClient.ts | ||
| --- | ||
| # tRPC -- Client Setup | ||
| ## Setup | ||
| ```ts | ||
| // server.ts | ||
| import { initTRPC } from '@trpc/server'; | ||
| import { z } from 'zod'; | ||
| const t = initTRPC.create(); | ||
| const appRouter = t.router({ | ||
| user: t.router({ | ||
| byId: t.procedure | ||
| .input(z.object({ id: z.string() })) | ||
| .query(({ input }) => ({ id: input.id, name: 'Bilbo' })), | ||
| create: t.procedure | ||
| .input(z.object({ name: z.string() })) | ||
| .mutation(({ input }) => ({ id: '1', ...input })), | ||
| }), | ||
| }); | ||
| export type AppRouter = typeof appRouter; | ||
| ``` | ||
| ```ts | ||
| // client.ts | ||
| import { createTRPCClient, httpBatchLink } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| }), | ||
| ], | ||
| }); | ||
| const user = await client.user.byId.query({ id: '1' }); | ||
| const created = await client.user.create.mutate({ name: 'Frodo' }); | ||
| ``` | ||
| ## Core Patterns | ||
| ### Dynamic Auth Headers | ||
| ```ts | ||
| import { createTRPCClient, httpBatchLink } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| let token = ''; | ||
| export function setToken(newToken: string) { | ||
| token = newToken; | ||
| } | ||
| export const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| headers() { | ||
| return { | ||
| Authorization: token ? `Bearer ${token}` : '', | ||
| }; | ||
| }, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| The `headers` callback is invoked on every HTTP request, so token changes take effect immediately. | ||
| ### Inferring Procedure Input and Output Types | ||
| ```ts | ||
| import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; | ||
| import type { AppRouter } from './server'; | ||
| type RouterInput = inferRouterInputs<AppRouter>; | ||
| type RouterOutput = inferRouterOutputs<AppRouter>; | ||
| type UserCreateInput = RouterInput['user']['create']; | ||
| type UserByIdOutput = RouterOutput['user']['byId']; | ||
| ``` | ||
| ### Aborting Requests with AbortController | ||
| ```ts | ||
| import { createTRPCClient, httpBatchLink } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })], | ||
| }); | ||
| const ac = new AbortController(); | ||
| const query = client.user.byId.query({ id: '1' }, { signal: ac.signal }); | ||
| ac.abort(); | ||
| ``` | ||
| ### Typed Error Handling | ||
| ```ts | ||
| import { TRPCClientError } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| function isTRPCClientError( | ||
| cause: unknown, | ||
| ): cause is TRPCClientError<AppRouter> { | ||
| return cause instanceof TRPCClientError; | ||
| } | ||
| try { | ||
| await client.user.byId.query({ id: '1' }); | ||
| } catch (cause) { | ||
| if (isTRPCClientError(cause)) { | ||
| console.log('tRPC error code:', cause.data?.code); | ||
| } | ||
| } | ||
| ``` | ||
| ## Common Mistakes | ||
| ### [CRITICAL] Missing AppRouter type parameter on createTRPCClient | ||
| Wrong: | ||
| ```ts | ||
| const client = createTRPCClient({ links: [httpBatchLink({ url })] }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url })] }); | ||
| ``` | ||
| Without the type parameter, all procedure calls return `any` and type safety is completely lost. | ||
| Source: www/docs/client/vanilla/setup.mdx | ||
| ### [CRITICAL] Transformer goes on individual links, not createTRPCClient | ||
| In v11, the `transformer` option is on individual terminating links, not the client constructor: | ||
| ```ts | ||
| import superjson from 'superjson'; | ||
| createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000', | ||
| transformer: superjson, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| In v11, the `transformer` option was moved from the client constructor to individual terminating links. Passing it to `createTRPCClient` throws a TypeError. | ||
| Source: packages/client/src/internals/TRPCUntypedClient.ts | ||
| ### [CRITICAL] Transformer on server but not on client links | ||
| Wrong: | ||
| ```ts | ||
| // Server: initTRPC.create({ transformer: superjson }) | ||
| // Client: | ||
| httpBatchLink({ url: 'http://localhost:3000' }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| // Server: initTRPC.create({ transformer: superjson }) | ||
| // Client: | ||
| httpBatchLink({ url: 'http://localhost:3000', transformer: superjson }); | ||
| ``` | ||
| If the server uses a transformer, every terminating link on the client must also specify that transformer. Mismatch causes "Unable to transform response" errors. | ||
| Source: https://github.com/trpc/trpc/issues/7083 | ||
| ### [CRITICAL] Using import instead of import type for AppRouter | ||
| Wrong: | ||
| ```ts | ||
| import { AppRouter } from '../server/router'; | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| import type { AppRouter } from '../server/router'; | ||
| ``` | ||
| A non-type import pulls the entire server bundle into the client. Use `import type` so it is erased at build time. | ||
| Source: www/docs/client/vanilla/setup.mdx | ||
| ### [CRITICAL] Importing appRouter value to derive type in client | ||
| Wrong: | ||
| ```ts | ||
| import { appRouter } from '../server/router'; | ||
| type AppRouter = typeof appRouter; | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| // In server: export type AppRouter = typeof appRouter; | ||
| // In client: | ||
| import type { AppRouter } from '../server/router'; | ||
| ``` | ||
| Importing the `appRouter` value (not just the type) bundles the entire server into the client, shipping server code to the browser. | ||
| Source: www/docs/server/routers.md | ||
| ### [CRITICAL] Using type assertions to bypass AppRouter import errors | ||
| Wrong: | ||
| ```ts | ||
| const client = createTRPCClient<any>({ links: [httpBatchLink({ url })] }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| // Fix the import path or monorepo configuration | ||
| import type { AppRouter } from '@myorg/api-types'; | ||
| const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url })] }); | ||
| ``` | ||
| Casting to `any` or manually recreating the router type destroys end-to-end type safety. Fix the import path or monorepo config instead. | ||
| Source: www/docs/client/vanilla/setup.mdx | ||
| ### [CRITICAL] Using createTRPCProxyClient (renamed in v11) | ||
| Wrong: | ||
| ```ts | ||
| import { createTRPCProxyClient } from '@trpc/client'; | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| import { createTRPCClient } from '@trpc/client'; | ||
| ``` | ||
| `createTRPCProxyClient` was renamed to `createTRPCClient` in v11. | ||
| Source: www/docs/client/vanilla/setup.mdx | ||
| ### [CRITICAL] Treating tRPC as a REST API | ||
| Wrong: | ||
| ```ts | ||
| fetch('/api/trpc/users/123', { method: 'GET' }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| const user = await client.user.byId.query({ id: '123' }); | ||
| // Raw equivalent: GET /api/trpc/user.byId?input={"id":"123"} | ||
| ``` | ||
| tRPC uses JSON-RPC over HTTP. Procedures are called by dot-separated name with JSON input, not by REST resource paths. | ||
| Source: www/docs/client/overview.md | ||
| ### [HIGH] HTML error page instead of JSON response | ||
| If you see `couldn't parse JSON, invalid character '<'`, the tRPC endpoint returned an HTML page (404/503) instead of JSON. This means the `url` in your link config is wrong or infrastructure routing is misconfigured -- it is not a tRPC bug. Verify the URL matches your adapter's mount point. | ||
| Source: www/docs/client/vanilla/setup.mdx | ||
| ## See Also | ||
| - `links` -- configure httpBatchLink, httpLink, splitLink, and other link types | ||
| - `superjson` -- set up SuperJSON transformer on server and client | ||
| - `server-setup` -- define routers, procedures, context, and export AppRouter type | ||
| - `react-query-setup` -- use tRPC with TanStack React Query for React applications |
| # Link Options Reference | ||
| ## httpLink | ||
| Terminating link that sends one tRPC operation per HTTP request. | ||
| ```ts | ||
| import { httpLink } from '@trpc/client'; | ||
| httpLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| fetch: customFetch, | ||
| transformer: superjson, | ||
| headers: { Authorization: 'Bearer token' }, | ||
| methodOverride: 'POST', | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | ---------------- | --------------------------------------------------------------------------------- | -------------- | --------------------------------------------- | | ||
| | `url` | `string \| URL` | required | Server endpoint URL | | ||
| | `fetch` | `typeof fetch` | global `fetch` | Fetch ponyfill | | ||
| | `transformer` | `DataTransformerOptions` | none | Data transformer (e.g. superjson) | | ||
| | `headers` | `HTTPHeaders \| (opts: { op: Operation }) => HTTPHeaders \| Promise<HTTPHeaders>` | `{}` | Static headers object or per-request callback | | ||
| | `methodOverride` | `'POST'` | none | Force all requests as POST | | ||
| ## httpBatchLink | ||
| Terminating link that batches multiple operations into a single HTTP request. | ||
| ```ts | ||
| import { httpBatchLink } from '@trpc/client'; | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| maxURLLength: 2083, | ||
| maxItems: 10, | ||
| headers({ opList }) { | ||
| return { Authorization: `Bearer ${opList[0]?.context.token}` }; | ||
| }, | ||
| transformer: superjson, | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | ---------------- | --------------------------------------------------------------------------------------- | -------------- | ---------------------------------------------------- | | ||
| | `url` | `string \| URL` | required | Server endpoint URL | | ||
| | `fetch` | `typeof fetch` | global `fetch` | Fetch ponyfill | | ||
| | `transformer` | `DataTransformerOptions` | none | Data transformer | | ||
| | `headers` | `HTTPHeaders \| (opts: { opList: Operation[] }) => HTTPHeaders \| Promise<HTTPHeaders>` | `{}` | Headers callback receives `opList` (array), not `op` | | ||
| | `maxURLLength` | `number` | `Infinity` | Split batch if URL exceeds this length | | ||
| | `maxItems` | `number` | `Infinity` | Maximum operations per batch | | ||
| | `methodOverride` | `'POST'` | none | Force all requests as POST | | ||
| ## httpBatchStreamLink | ||
| Terminating link similar to httpBatchLink but streams responses as they arrive instead of waiting for all to complete. | ||
| ```ts | ||
| import { httpBatchStreamLink } from '@trpc/client'; | ||
| httpBatchStreamLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| maxURLLength: 2083, | ||
| maxItems: 10, | ||
| transformer: superjson, | ||
| streamHeader: 'accept', | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | --------------------------- | --------------------------- | --------------- | ------------------------------------------------------------------------------------------------- | | ||
| | All `httpBatchLink` options | | | Inherits all httpBatchLink options | | ||
| | `streamHeader` | `'trpc-accept' \| 'accept'` | `'trpc-accept'` | Header used to signal streaming. Use `'accept'` to avoid CORS preflight on cross-origin requests. | | ||
| Sends `trpc-accept: application/jsonl` (or `Accept: application/jsonl`). Response arrives as `transfer-encoding: chunked` with `content-type: application/jsonl`. Cannot set response headers (including cookies) after stream begins. | ||
| ## splitLink | ||
| Non-terminating link that branches the link chain based on a condition. | ||
| ```ts | ||
| import { | ||
| httpBatchLink, | ||
| httpLink, | ||
| httpSubscriptionLink, | ||
| splitLink, | ||
| } from '@trpc/client'; | ||
| splitLink({ | ||
| condition: (op) => op.type === 'subscription', | ||
| true: httpSubscriptionLink({ url }), | ||
| false: httpBatchLink({ url }), | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | ----------- | ---------------------------- | -------- | ------------------------------------------------------------- | | ||
| | `condition` | `(op: Operation) => boolean` | required | Route predicate | | ||
| | `true` | `TRPCLink \| TRPCLink[]` | required | Link(s) for condition=true. Must include a terminating link. | | ||
| | `false` | `TRPCLink \| TRPCLink[]` | required | Link(s) for condition=false. Must include a terminating link. | | ||
| Each branch creates its own sub-chain, so both branches need a terminating link. | ||
| ## loggerLink | ||
| Non-terminating link that logs operations to the console. | ||
| ```ts | ||
| import { loggerLink } from '@trpc/client'; | ||
| loggerLink({ | ||
| enabled: (opts) => | ||
| (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') || | ||
| (opts.direction === 'down' && opts.result instanceof Error), | ||
| colorMode: 'ansi', | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | ------------- | -------------------------------------------------------------------- | ------------------------------------ | -------------------------------- | | ||
| | `enabled` | `(opts: { direction: 'up' \| 'down'; result?: unknown }) => boolean` | `() => true` | Control when logging is active | | ||
| | `logger` | `(opts: LoggerOpts) => void` | built-in pretty logger | Custom log function | | ||
| | `console` | `{ log: Function; error: Function }` | `globalThis.console` | Console implementation | | ||
| | `colorMode` | `'ansi' \| 'css' \| 'none'` | `'css'` in browser, `'ansi'` in Node | Color output mode | | ||
| | `withContext` | `boolean` | `false` (true if css) | Include operation context in log | | ||
| ## retryLink | ||
| Non-terminating link that retries failed operations. | ||
| ```ts | ||
| import { retryLink } from '@trpc/client'; | ||
| retryLink({ | ||
| retry(opts) { | ||
| if (opts.error.data?.code === 'INTERNAL_SERVER_ERROR') { | ||
| return opts.attempts <= 3; | ||
| } | ||
| return false; | ||
| }, | ||
| retryDelayMs: (attempt) => Math.min(1000 * 2 ** attempt, 30000), | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | -------------- | -------------------------------------------- | --------- | --------------------------- | | ||
| | `retry` | `(opts: { op, error, attempts }) => boolean` | required | Return true to retry | | ||
| | `retryDelayMs` | `(attempt: number) => number` | `() => 0` | Delay between retries in ms | | ||
| When used with subscriptions that use `tracked()`, automatically includes the last known event ID on retry. | ||
| ## wsLink | ||
| Terminating link for WebSocket connections. Requires a `TRPCWebSocketClient`. | ||
| ```ts | ||
| import { createWSClient, wsLink } from '@trpc/client'; | ||
| const wsClient = createWSClient({ | ||
| url: 'ws://localhost:3000', | ||
| connectionParams: () => ({ token: 'supersecret' }), | ||
| lazy: { enabled: true, closeMs: 10_000 }, | ||
| keepAlive: { enabled: true, intervalMs: 5_000, pongTimeoutMs: 1_000 }, | ||
| }); | ||
| wsLink<AppRouter>({ | ||
| client: wsClient, | ||
| transformer: superjson, | ||
| }); | ||
| ``` | ||
| ### wsLink Options | ||
| | Option | Type | Default | Description | | ||
| | ------------- | ------------------------ | -------- | ------------------------------------ | | ||
| | `client` | `TRPCWebSocketClient` | required | WebSocket client from createWSClient | | ||
| | `transformer` | `DataTransformerOptions` | none | Data transformer | | ||
| ### createWSClient Options | ||
| | Option | Type | Default | Description | | ||
| | ------------------------- | ---------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------- | | ||
| | `url` | `string \| (() => MaybePromise<string>)` | required | WebSocket server URL | | ||
| | `connectionParams` | `Record<string, string> \| null \| (() => MaybePromise<Record<string, string> \| null>)` | `null` | Auth params sent as first message, available in `createContext()` | | ||
| | `WebSocket` | `typeof WebSocket` | global `WebSocket` | WebSocket ponyfill | | ||
| | `retryDelayMs` | `(attemptIndex: number) => number` | exponential backoff | Reconnection delay | | ||
| | `onOpen` | `() => void` | none | Connection opened callback | | ||
| | `onError` | `(evt?: Event) => void` | none | Connection error callback | | ||
| | `onClose` | `(cause?: { code?: number }) => void` | none | Connection closed callback | | ||
| | `lazy.enabled` | `boolean` | `false` | Close WS after inactivity | | ||
| | `lazy.closeMs` | `number` | `0` | Idle timeout before closing | | ||
| | `keepAlive.enabled` | `boolean` | `false` | Send ping messages | | ||
| | `keepAlive.intervalMs` | `number` | `5000` | Ping interval | | ||
| | `keepAlive.pongTimeoutMs` | `number` | `1000` | Close if no pong within this time | | ||
| ## httpSubscriptionLink | ||
| Terminating link for Server-Sent Events (SSE) subscriptions. | ||
| ```ts | ||
| import { httpSubscriptionLink } from '@trpc/client'; | ||
| import { EventSourcePolyfill } from 'event-source-polyfill'; | ||
| httpSubscriptionLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| connectionParams: async () => ({ token: 'supersecret' }), | ||
| transformer: superjson, | ||
| EventSource: EventSourcePolyfill, | ||
| eventSourceOptions: async ({ op }) => ({ | ||
| headers: { | ||
| authorization: 'Bearer token', | ||
| }, | ||
| }), | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | -------------------- | ------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------- | | ||
| | `url` | `string \| (() => string \| Promise<string>)` | required | Server endpoint URL | | ||
| | `connectionParams` | `Record<string, string> \| null \| (() => MaybePromise<...>)` | none | Serialized as URL query param | | ||
| | `transformer` | `DataTransformerOptions` | none | Data transformer | | ||
| | `EventSource` | EventSource constructor | global `EventSource` | EventSource ponyfill for custom headers | | ||
| | `eventSourceOptions` | `EventSourceInit \| ((opts: { op }) => EventSourceInit \| Promise<EventSourceInit>)` | none | Options passed to EventSource constructor | | ||
| For cross-domain cookies, use `eventSourceOptions: () => ({ withCredentials: true })`. | ||
| ## unstable_localLink | ||
| Terminating link for direct procedure calls without HTTP. Useful for testing and server-side usage. | ||
| ```ts | ||
| import { unstable_localLink } from '@trpc/client'; | ||
| import { appRouter } from './server'; | ||
| unstable_localLink({ | ||
| router: appRouter, | ||
| createContext: async () => ({ db: prisma }), | ||
| onError: (opts) => console.error('Error:', opts.error), | ||
| }); | ||
| ``` | ||
| | Option | Type | Default | Description | | ||
| | --------------- | ------------------------------------- | -------- | ------------------------ | | ||
| | `router` | `AnyRouter` | required | tRPC router instance | | ||
| | `createContext` | `() => Promise<Context>` | required | Context factory per call | | ||
| | `onError` | `(opts: ErrorHandlerOptions) => void` | none | Error handler | | ||
| | `transformer` | `DataTransformerOptions` | none | Data transformer | |
| --- | ||
| name: links | ||
| description: > | ||
| Configure the tRPC client link chain: httpLink, httpBatchLink, | ||
| httpBatchStreamLink, splitLink, loggerLink, wsLink, createWSClient, | ||
| httpSubscriptionLink, unstable_localLink, retryLink. Choose the right | ||
| terminating link. Route subscriptions via splitLink. Build custom links | ||
| for SOA routing. Link options: url, headers, transformer, maxURLLength, | ||
| maxItems, connectionParams, EventSource ponyfill. | ||
| type: core | ||
| library: trpc | ||
| library_version: '11.14.0' | ||
| requires: | ||
| - client-setup | ||
| sources: | ||
| - www/docs/client/links/overview.md | ||
| - www/docs/client/links/httpLink.md | ||
| - www/docs/client/links/httpBatchLink.md | ||
| - www/docs/client/links/httpBatchStreamLink.md | ||
| - www/docs/client/links/splitLink.mdx | ||
| - www/docs/client/links/wsLink.md | ||
| - www/docs/client/links/httpSubscriptionLink.md | ||
| - www/docs/client/links/localLink.mdx | ||
| - www/docs/client/links/loggerLink.md | ||
| - packages/client/src/links/ | ||
| --- | ||
| # tRPC -- Links | ||
| ## Setup | ||
| ```ts | ||
| import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| loggerLink(), | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| The `links` array is a chain: non-terminating links (loggerLink, splitLink, retryLink) forward operations; the chain must end with a terminating link (httpBatchLink, httpLink, httpBatchStreamLink, wsLink, httpSubscriptionLink, unstable_localLink). | ||
| ## Core Patterns | ||
| ### httpBatchLink -- Batch Multiple Calls into One Request | ||
| ```ts | ||
| import { createTRPCClient, httpBatchLink } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| maxURLLength: 2083, | ||
| maxItems: 10, | ||
| }), | ||
| ], | ||
| }); | ||
| const [post1, post2, post3] = await Promise.all([ | ||
| client.post.byId.query(1), | ||
| client.post.byId.query(2), | ||
| client.post.byId.query(3), | ||
| ]); | ||
| ``` | ||
| Concurrent calls are batched into a single HTTP request. Set `maxURLLength` to prevent 414 errors from long URLs. | ||
| ### splitLink -- Route Subscriptions to SSE | ||
| ```ts | ||
| import { | ||
| createTRPCClient, | ||
| httpBatchLink, | ||
| httpSubscriptionLink, | ||
| splitLink, | ||
| } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| splitLink({ | ||
| condition: (op) => op.type === 'subscription', | ||
| true: httpSubscriptionLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| }), | ||
| false: httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| }), | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| ### splitLink -- Disable Batching Per-Request via Context | ||
| ```ts | ||
| import { | ||
| createTRPCClient, | ||
| httpBatchLink, | ||
| httpLink, | ||
| splitLink, | ||
| } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| splitLink({ | ||
| condition: (op) => Boolean(op.context.skipBatch), | ||
| true: httpLink({ url: 'http://localhost:3000/trpc' }), | ||
| false: httpBatchLink({ url: 'http://localhost:3000/trpc' }), | ||
| }), | ||
| ], | ||
| }); | ||
| const result = await client.post.byId.query(1, { | ||
| context: { skipBatch: true }, | ||
| }); | ||
| ``` | ||
| ### httpBatchStreamLink -- Stream Responses as They Arrive | ||
| ```ts | ||
| import { createTRPCClient, httpBatchStreamLink } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchStreamLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| }), | ||
| ], | ||
| }); | ||
| const iterable = await client.examples.iterable.query(); | ||
| for await (const value of iterable) { | ||
| console.log(value); | ||
| } | ||
| ``` | ||
| ### wsLink -- WebSocket Terminating Link | ||
| ```ts | ||
| import { createTRPCClient, createWSClient, wsLink } from '@trpc/client'; | ||
| import type { AppRouter } from './server'; | ||
| const wsClient = createWSClient({ | ||
| url: 'ws://localhost:3000', | ||
| }); | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [wsLink<AppRouter>({ client: wsClient })], | ||
| }); | ||
| ``` | ||
| ### Custom Link | ||
| ```ts | ||
| import { TRPCLink } from '@trpc/client'; | ||
| import { observable } from '@trpc/server/observable'; | ||
| import type { AppRouter } from './server'; | ||
| export const timingLink: TRPCLink<AppRouter> = () => { | ||
| return ({ next, op }) => { | ||
| return observable((observer) => { | ||
| const start = Date.now(); | ||
| const unsubscribe = next(op).subscribe({ | ||
| next(value) { | ||
| observer.next(value); | ||
| }, | ||
| error(err) { | ||
| console.error(`${op.path} failed in ${Date.now() - start}ms`); | ||
| observer.error(err); | ||
| }, | ||
| complete() { | ||
| console.log(`${op.path} completed in ${Date.now() - start}ms`); | ||
| observer.complete(); | ||
| }, | ||
| }); | ||
| return unsubscribe; | ||
| }); | ||
| }; | ||
| }; | ||
| ``` | ||
| ## Common Mistakes | ||
| ### [CRITICAL] No terminating link in the chain | ||
| Wrong: | ||
| ```ts | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [loggerLink()], | ||
| }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [loggerLink(), httpBatchLink({ url: 'http://localhost:3000/trpc' })], | ||
| }); | ||
| ``` | ||
| The link chain must end with a terminating link. Without one, tRPC throws "No more links to execute - did you forget to add an ending link?" | ||
| Source: packages/client/src/links/internals/createChain.ts | ||
| ### [CRITICAL] Sending subscriptions through httpLink or httpBatchLink | ||
| Wrong: | ||
| ```ts | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })], | ||
| }); | ||
| await client.onMessage.subscribe({}); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| splitLink({ | ||
| condition: (op) => op.type === 'subscription', | ||
| true: httpSubscriptionLink({ url: 'http://localhost:3000/trpc' }), | ||
| false: httpBatchLink({ url: 'http://localhost:3000/trpc' }), | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| httpLink and httpBatchLink throw on subscription operations. Subscriptions must use httpSubscriptionLink or wsLink, routed via splitLink. | ||
| Source: packages/client/src/links/httpLink.ts | ||
| ### [HIGH] httpBatchLink and httpBatchStreamLink headers callback receives { opList } | ||
| `httpBatchLink` and `httpBatchStreamLink` headers callbacks receive `{ opList }` (a `NonEmptyArray<Operation>`), not `{ op }` like `httpLink`. Access per-operation context via `opList[0]?.context`: | ||
| ```ts | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| headers({ opList }) { | ||
| return { authorization: opList[0]?.context.token }; | ||
| }, | ||
| }); | ||
| ``` | ||
| `httpBatchLink` headers callback receives `{ opList }` (an array of operations) | ||
| Source: packages/client/src/links/httpBatchLink.ts | ||
| ### [MEDIUM] Default batch limits are Infinity | ||
| Wrong: | ||
| ```ts | ||
| httpBatchLink({ url: 'http://localhost:3000/trpc' }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| maxURLLength: 2083, | ||
| maxItems: 10, | ||
| }); | ||
| ``` | ||
| Both `maxURLLength` and `maxItems` default to `Infinity`, which can cause 413/414 HTTP errors on servers or CDNs with URL length limits. | ||
| Source: packages/client/src/links/httpBatchLink.ts | ||
| ### [HIGH] httpBatchStreamLink data loss on stream completion | ||
| There is a known race condition where buffered chunks can be lost on normal stream completion. Long streaming responses (e.g., LLM output) may be truncated. If you experience truncated data, switch to `httpBatchLink` for those operations. | ||
| Source: https://github.com/trpc/trpc/issues/7209 | ||
| ## References | ||
| - [Link options reference](references/link-options.md) | ||
| ## See Also | ||
| - `client-setup` -- create the tRPC client and configure links | ||
| - `superjson` -- add transformer to links for Date/Map/Set support | ||
| - `subscriptions` -- set up SSE or WebSocket real-time streams | ||
| - `non-json-content-types` -- route FormData/binary through splitLink + httpLink | ||
| - `service-oriented-architecture` -- build custom routing links for multi-service backends |
| --- | ||
| name: superjson | ||
| description: > | ||
| Configure SuperJSON transformer on both server initTRPC.create({ transformer: | ||
| superjson }) and every client terminating link (httpBatchLink, httpLink, wsLink, | ||
| httpSubscriptionLink) to support Date, Map, Set, BigInt over the wire. Transformer | ||
| must match on both sides. In v11, transformer goes on individual links, not the | ||
| client constructor. | ||
| type: composition | ||
| library: trpc | ||
| library_version: '11.14.0' | ||
| requires: | ||
| - server-setup | ||
| - client-setup | ||
| sources: | ||
| - www/docs/server/data-transformers.md | ||
| --- | ||
| # tRPC -- SuperJSON Transformer | ||
| ## Setup | ||
| ### 1. Install superjson | ||
| ```bash | ||
| npm install superjson | ||
| ``` | ||
| ### 2. Add to initTRPC on the server | ||
| ```ts | ||
| // server/trpc.ts | ||
| import { initTRPC } from '@trpc/server'; | ||
| import superjson from 'superjson'; | ||
| const t = initTRPC.create({ | ||
| transformer: superjson, | ||
| }); | ||
| export const router = t.router; | ||
| export const publicProcedure = t.procedure; | ||
| ``` | ||
| ### 3. Add to every terminating link on the client | ||
| ```ts | ||
| // client.ts | ||
| import { createTRPCClient, httpBatchLink } from '@trpc/client'; | ||
| import superjson from 'superjson'; | ||
| import type { AppRouter } from './server/trpc'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| Now Date, Map, Set, BigInt, RegExp, undefined, and other non-JSON types survive the round trip. | ||
| ## Core Patterns | ||
| ### SuperJSON with splitLink and Subscriptions | ||
| ```ts | ||
| import { | ||
| createTRPCClient, | ||
| httpBatchLink, | ||
| httpSubscriptionLink, | ||
| splitLink, | ||
| } from '@trpc/client'; | ||
| import superjson from 'superjson'; | ||
| import type { AppRouter } from './server/trpc'; | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| splitLink({ | ||
| condition: (op) => op.type === 'subscription', | ||
| true: httpSubscriptionLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| false: httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| Every terminating link in every branch must have `transformer: superjson`. | ||
| ### SuperJSON with wsLink | ||
| ```ts | ||
| import { createTRPCClient, createWSClient, wsLink } from '@trpc/client'; | ||
| import superjson from 'superjson'; | ||
| import type { AppRouter } from './server/trpc'; | ||
| const wsClient = createWSClient({ | ||
| url: 'ws://localhost:3000', | ||
| }); | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| wsLink<AppRouter>({ | ||
| client: wsClient, | ||
| transformer: superjson, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| ### Returning Dates from Procedures | ||
| ```ts | ||
| // server | ||
| import { z } from 'zod'; | ||
| import { publicProcedure, router } from './trpc'; | ||
| const appRouter = router({ | ||
| getEvent: publicProcedure | ||
| .input(z.object({ id: z.string() })) | ||
| .query(({ input }) => { | ||
| return { | ||
| id: input.id, | ||
| name: 'Launch Party', | ||
| date: new Date('2025-01-01T00:00:00Z'), | ||
| }; | ||
| }), | ||
| }); | ||
| export type AppRouter = typeof appRouter; | ||
| ``` | ||
| ```ts | ||
| // client | ||
| const event = await client.getEvent.query({ id: '1' }); | ||
| console.log(event.date instanceof Date); // true | ||
| console.log(event.date.getFullYear()); // 2025 | ||
| ``` | ||
| Without superjson, `event.date` would be a string like `"2025-01-01T00:00:00.000Z"`. | ||
| ## Common Mistakes | ||
| ### [CRITICAL] Transformer on server but missing from client link | ||
| Wrong: | ||
| ```ts | ||
| // Server | ||
| const t = initTRPC.create({ transformer: superjson }); | ||
| // Client | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })], | ||
| }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| // Server | ||
| const t = initTRPC.create({ transformer: superjson }); | ||
| // Client | ||
| const client = createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| Server encodes with superjson but client tries to parse raw JSON, causing "Unable to transform response" or garbled data. | ||
| Source: www/docs/server/data-transformers.md | ||
| ### [CRITICAL] Transformer goes on individual links, not createTRPCClient | ||
| The `transformer` option is on individual terminating links: | ||
| ```ts | ||
| createTRPCClient<AppRouter>({ | ||
| links: [ | ||
| httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| In v11, `transformer` was moved from the client constructor to individual links. Passing it to `createTRPCClient` throws a TypeError. | ||
| Source: packages/client/src/internals/TRPCUntypedClient.ts | ||
| ### [CRITICAL] Transformer on only some terminating links in splitLink | ||
| Wrong: | ||
| ```ts | ||
| splitLink({ | ||
| condition: (op) => op.type === 'subscription', | ||
| true: httpSubscriptionLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| // missing transformer! | ||
| }), | ||
| false: httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| splitLink({ | ||
| condition: (op) => op.type === 'subscription', | ||
| true: httpSubscriptionLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| false: httpBatchLink({ | ||
| url: 'http://localhost:3000/trpc', | ||
| transformer: superjson, | ||
| }), | ||
| }); | ||
| ``` | ||
| Every terminating link must have the same transformer. A missing transformer on one branch causes deserialization failures only for operations routed through that branch. | ||
| Source: www/docs/server/data-transformers.md | ||
| ### [HIGH] Using transformer on client but not on server | ||
| Wrong: | ||
| ```ts | ||
| // Server -- no transformer | ||
| const t = initTRPC.create(); | ||
| // Client | ||
| httpBatchLink({ url, transformer: superjson }); | ||
| ``` | ||
| Correct: | ||
| ```ts | ||
| // Server | ||
| const t = initTRPC.create({ transformer: superjson }); | ||
| // Client | ||
| httpBatchLink({ url, transformer: superjson }); | ||
| ``` | ||
| The transformer must be configured on both `initTRPC.create()` and every client link. Client-only transformer corrupts the request encoding because the server expects plain JSON. | ||
| Source: www/docs/server/data-transformers.md | ||
| ## See Also | ||
| - `client-setup` -- create the tRPC client and configure links | ||
| - `links` -- detailed options for each link type including transformer | ||
| - `server-setup` -- initTRPC.create() where the server transformer is configured |
+3
-3
| import { __commonJS, __toESM, require_defineProperty, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs"; | ||
| import { createChain, splitLink } from "./splitLink-B7Cuf2c_.mjs"; | ||
| import { TRPCClientError, isTRPCClientError } from "./TRPCClientError-apv8gw59.mjs"; | ||
| import { fetchHTTPResponse, getBody, getFetch, getUrl, resolveHTTPLinkOptions } from "./httpUtils-pyf5RF99.mjs"; | ||
| import { httpLink, isFormData, isNonJsonSerializable, isOctetType } from "./httpLink-lG_6juPY.mjs"; | ||
| import { abortSignalToPromise, allAbortSignals, dataLoader, httpBatchLink, raceAbortSignals } from "./httpBatchLink-LhidKAPw.mjs"; | ||
| import { fetchHTTPResponse, getBody, getFetch, getUrl, resolveHTTPLinkOptions } from "./httpUtils-BNq9QC3d.mjs"; | ||
| import { httpLink, isFormData, isNonJsonSerializable, isOctetType } from "./httpLink-oiU8eqFi.mjs"; | ||
| import { abortSignalToPromise, allAbortSignals, dataLoader, httpBatchLink, raceAbortSignals } from "./httpBatchLink-CaWjh1oW.mjs"; | ||
| import { getTransformer } from "./unstable-internals-Bg7n9BBj.mjs"; | ||
@@ -8,0 +8,0 @@ import { loggerLink } from "./loggerLink-ineCN1PO.mjs"; |
| import "../objectSpread2-BvkFp-_Y.mjs"; | ||
| import "../TRPCClientError-apv8gw59.mjs"; | ||
| import "../httpUtils-pyf5RF99.mjs"; | ||
| import { httpBatchLink } from "../httpBatchLink-LhidKAPw.mjs"; | ||
| import "../httpUtils-BNq9QC3d.mjs"; | ||
| import { httpBatchLink } from "../httpBatchLink-CaWjh1oW.mjs"; | ||
| import "../unstable-internals-Bg7n9BBj.mjs"; | ||
| export { httpBatchLink }; |
| import "../objectSpread2-BvkFp-_Y.mjs"; | ||
| import "../TRPCClientError-apv8gw59.mjs"; | ||
| import "../httpUtils-pyf5RF99.mjs"; | ||
| import { httpLink } from "../httpLink-lG_6juPY.mjs"; | ||
| import "../httpUtils-BNq9QC3d.mjs"; | ||
| import { httpLink } from "../httpLink-oiU8eqFi.mjs"; | ||
| import "../unstable-internals-Bg7n9BBj.mjs"; | ||
| export { httpLink }; |
+15
-5
| { | ||
| "name": "@trpc/client", | ||
| "type": "module", | ||
| "version": "11.14.0", | ||
| "version": "11.14.1-canary.0+e39a654b3", | ||
| "description": "The tRPC client library", | ||
@@ -112,10 +112,14 @@ "author": "KATT", | ||
| "!**/*.test.*", | ||
| "!**/__tests__" | ||
| "!**/__tests__", | ||
| "skills", | ||
| "!skills/_artifacts", | ||
| "bin" | ||
| ], | ||
| "peerDependencies": { | ||
| "@trpc/server": "11.14.0", | ||
| "@trpc/server": "11.14.1", | ||
| "typescript": ">=5.7.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@trpc/server": "11.14.0", | ||
| "@tanstack/intent": "^0.0.20", | ||
| "@trpc/server": "11.14.1", | ||
| "@types/isomorphic-fetch": "^0.0.39", | ||
@@ -139,3 +143,9 @@ "@types/node": "^22.13.5", | ||
| ], | ||
| "gitHead": "6e03f5c2f8d8ebaa237747d2db447737393402c6" | ||
| "keywords": [ | ||
| "tanstack-intent" | ||
| ], | ||
| "bin": { | ||
| "intent": "./bin/intent.js" | ||
| }, | ||
| "gitHead": "e39a654b306d0c75730a9c546e77333f9d1dca8c" | ||
| } |
+8
-0
@@ -39,2 +39,10 @@ <p align="center"> | ||
| ## AI Agents | ||
| If you use an AI coding agent, install tRPC skills for better code generation: | ||
| ```bash | ||
| npx @tanstack/intent@latest install | ||
| ``` | ||
| ## Basic Example | ||
@@ -41,0 +49,0 @@ |
| import { __toESM, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs"; | ||
| import { TRPCClientError } from "./TRPCClientError-apv8gw59.mjs"; | ||
| import { getUrl, jsonHttpRequester, resolveHTTPLinkOptions } from "./httpUtils-pyf5RF99.mjs"; | ||
| import { observable } from "@trpc/server/observable"; | ||
| import { transformResult } from "@trpc/server/unstable-core-do-not-import"; | ||
| //#region src/internals/dataLoader.ts | ||
| /** | ||
| * A function that should never be called unless we messed something up. | ||
| */ | ||
| const throwFatalError = () => { | ||
| throw new Error("Something went wrong. Please submit an issue at https://github.com/trpc/trpc/issues/new"); | ||
| }; | ||
| /** | ||
| * Dataloader that's very inspired by https://github.com/graphql/dataloader | ||
| * Less configuration, no caching, and allows you to cancel requests | ||
| * When cancelling a single fetch the whole batch will be cancelled only when _all_ items are cancelled | ||
| */ | ||
| function dataLoader(batchLoader) { | ||
| let pendingItems = null; | ||
| let dispatchTimer = null; | ||
| const destroyTimerAndPendingItems = () => { | ||
| clearTimeout(dispatchTimer); | ||
| dispatchTimer = null; | ||
| pendingItems = null; | ||
| }; | ||
| /** | ||
| * Iterate through the items and split them into groups based on the `batchLoader`'s validate function | ||
| */ | ||
| function groupItems(items) { | ||
| const groupedItems = [[]]; | ||
| let index = 0; | ||
| while (true) { | ||
| const item = items[index]; | ||
| if (!item) break; | ||
| const lastGroup = groupedItems[groupedItems.length - 1]; | ||
| if (item.aborted) { | ||
| var _item$reject; | ||
| (_item$reject = item.reject) === null || _item$reject === void 0 || _item$reject.call(item, new Error("Aborted")); | ||
| index++; | ||
| continue; | ||
| } | ||
| const isValid = batchLoader.validate(lastGroup.concat(item).map((it) => it.key)); | ||
| if (isValid) { | ||
| lastGroup.push(item); | ||
| index++; | ||
| continue; | ||
| } | ||
| if (lastGroup.length === 0) { | ||
| var _item$reject2; | ||
| (_item$reject2 = item.reject) === null || _item$reject2 === void 0 || _item$reject2.call(item, new Error("Input is too big for a single dispatch")); | ||
| index++; | ||
| continue; | ||
| } | ||
| groupedItems.push([]); | ||
| } | ||
| return groupedItems; | ||
| } | ||
| function dispatch() { | ||
| const groupedItems = groupItems(pendingItems); | ||
| destroyTimerAndPendingItems(); | ||
| for (const items of groupedItems) { | ||
| if (!items.length) continue; | ||
| const batch = { items }; | ||
| for (const item of items) item.batch = batch; | ||
| 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 { | ||
| var _item$resolve; | ||
| const value = await Promise.resolve(valueOrPromise); | ||
| (_item$resolve = item.resolve) === null || _item$resolve === void 0 || _item$resolve.call(item, value); | ||
| } catch (cause) { | ||
| var _item$reject3; | ||
| (_item$reject3 = item.reject) === null || _item$reject3 === void 0 || _item$reject3.call(item, cause); | ||
| } | ||
| item.batch = null; | ||
| item.reject = null; | ||
| item.resolve = null; | ||
| })); | ||
| for (const item of batch.items) { | ||
| var _item$reject4; | ||
| (_item$reject4 = item.reject) === null || _item$reject4 === void 0 || _item$reject4.call(item, new Error("Missing result")); | ||
| item.batch = null; | ||
| } | ||
| }).catch((cause) => { | ||
| for (const item of batch.items) { | ||
| var _item$reject5; | ||
| (_item$reject5 = item.reject) === null || _item$reject5 === void 0 || _item$reject5.call(item, cause); | ||
| item.batch = null; | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| function load(key) { | ||
| var _dispatchTimer; | ||
| const item = { | ||
| aborted: false, | ||
| key, | ||
| batch: null, | ||
| resolve: throwFatalError, | ||
| reject: throwFatalError | ||
| }; | ||
| const promise = new Promise((resolve, reject) => { | ||
| var _pendingItems; | ||
| item.reject = reject; | ||
| item.resolve = resolve; | ||
| (_pendingItems = pendingItems) !== null && _pendingItems !== void 0 || (pendingItems = []); | ||
| pendingItems.push(item); | ||
| }); | ||
| (_dispatchTimer = dispatchTimer) !== null && _dispatchTimer !== void 0 || (dispatchTimer = setTimeout(dispatch)); | ||
| return promise; | ||
| } | ||
| return { load }; | ||
| } | ||
| //#endregion | ||
| //#region src/internals/signals.ts | ||
| /** | ||
| * Like `Promise.all()` but for abort signals | ||
| * - When all signals have been aborted, the merged signal will be aborted | ||
| * - If one signal is `null`, no signal will be aborted | ||
| */ | ||
| function allAbortSignals(...signals) { | ||
| const ac = new AbortController(); | ||
| const count = signals.length; | ||
| let abortedCount = 0; | ||
| const onAbort = () => { | ||
| if (++abortedCount === count) ac.abort(); | ||
| }; | ||
| for (const signal of signals) if (signal === null || signal === void 0 ? void 0 : signal.aborted) onAbort(); | ||
| else signal === null || signal === void 0 || signal.addEventListener("abort", onAbort, { once: true }); | ||
| return ac.signal; | ||
| } | ||
| /** | ||
| * Like `Promise.race` but for abort signals | ||
| * | ||
| * Basically, a ponyfill for | ||
| * [`AbortSignal.any`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static). | ||
| */ | ||
| function raceAbortSignals(...signals) { | ||
| const ac = new AbortController(); | ||
| for (const signal of signals) if (signal === null || signal === void 0 ? void 0 : signal.aborted) ac.abort(); | ||
| else signal === null || signal === void 0 || signal.addEventListener("abort", () => ac.abort(), { once: true }); | ||
| return ac.signal; | ||
| } | ||
| function abortSignalToPromise(signal) { | ||
| return new Promise((_, reject) => { | ||
| if (signal.aborted) { | ||
| reject(signal.reason); | ||
| return; | ||
| } | ||
| signal.addEventListener("abort", () => { | ||
| reject(signal.reason); | ||
| }, { once: true }); | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/links/httpBatchLink.ts | ||
| var import_objectSpread2 = __toESM(require_objectSpread2(), 1); | ||
| /** | ||
| * @see https://trpc.io/docs/client/links/httpBatchLink | ||
| */ | ||
| function httpBatchLink(opts) { | ||
| var _opts$maxURLLength, _opts$maxItems; | ||
| const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
| const maxURLLength = (_opts$maxURLLength = opts.maxURLLength) !== null && _opts$maxURLLength !== void 0 ? _opts$maxURLLength : Infinity; | ||
| const maxItems = (_opts$maxItems = opts.maxItems) !== null && _opts$maxItems !== void 0 ? _opts$maxItems : Infinity; | ||
| return () => { | ||
| const batchLoader = (type) => { | ||
| return { | ||
| validate(batchOps) { | ||
| if (maxURLLength === Infinity && maxItems === Infinity) 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 = getUrl((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, 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((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, resolvedOpts), {}, { | ||
| path, | ||
| inputs, | ||
| type, | ||
| headers() { | ||
| if (!opts.headers) return {}; | ||
| if (typeof opts.headers === "function") return opts.headers({ opList: batchOps }); | ||
| return opts.headers; | ||
| }, | ||
| signal | ||
| })); | ||
| const resJSON = Array.isArray(res.json) ? res.json : batchOps.map(() => res.json); | ||
| const result = resJSON.map((item) => ({ | ||
| meta: res.meta, | ||
| json: item | ||
| })); | ||
| return result; | ||
| } | ||
| }; | ||
| }; | ||
| const query = dataLoader(batchLoader("query")); | ||
| const mutation = dataLoader(batchLoader("mutation")); | ||
| const loaders = { | ||
| query, | ||
| mutation | ||
| }; | ||
| return ({ op }) => { | ||
| return observable((observer) => { | ||
| /* istanbul ignore if -- @preserve */ | ||
| if (op.type === "subscription") throw new Error("Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`"); | ||
| const loader = loaders[op.type]; | ||
| const promise = loader.load(op); | ||
| let _res = void 0; | ||
| promise.then((res) => { | ||
| _res = res; | ||
| const transformed = transformResult(res.json, resolvedOpts.transformer.output); | ||
| 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 === null || _res === void 0 ? void 0 : _res.meta })); | ||
| }); | ||
| return () => {}; | ||
| }); | ||
| }; | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { abortSignalToPromise, allAbortSignals, dataLoader, httpBatchLink, raceAbortSignals }; | ||
| //# sourceMappingURL=httpBatchLink-LhidKAPw.mjs.map |
| {"version":3,"file":"httpBatchLink-LhidKAPw.mjs","names":["batchLoader: BatchLoader<TKey, TValue>","pendingItems: BatchItem<TKey, TValue>[] | null","dispatchTimer: ReturnType<typeof setTimeout> | null","items: BatchItem<TKey, TValue>[]","groupedItems: BatchItem<TKey, TValue>[][]","batch: Batch<TKey, TValue>","key: TKey","item: BatchItem<TKey, TValue>","signal: AbortSignal","opts: HTTPBatchLinkOptions<TRouter['_def']['_config']['$types']>","type: ProcedureType"],"sources":["../src/internals/dataLoader.ts","../src/internals/signals.ts","../src/links/httpBatchLink.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-non-null-assertion */\n\ntype BatchItem<TKey, TValue> = {\n aborted: boolean;\n key: TKey;\n resolve: ((value: TValue) => void) | null;\n reject: ((error: Error) => void) | null;\n batch: Batch<TKey, TValue> | null;\n};\ntype Batch<TKey, TValue> = {\n items: BatchItem<TKey, TValue>[];\n};\nexport type BatchLoader<TKey, TValue> = {\n validate: (keys: TKey[]) => boolean;\n fetch: (keys: TKey[]) => Promise<TValue[] | Promise<TValue>[]>;\n};\n\n/**\n * A function that should never be called unless we messed something up.\n */\nconst throwFatalError = () => {\n throw new Error(\n 'Something went wrong. Please submit an issue at https://github.com/trpc/trpc/issues/new',\n );\n};\n\n/**\n * Dataloader that's very inspired by https://github.com/graphql/dataloader\n * Less configuration, no caching, and allows you to cancel requests\n * When cancelling a single fetch the whole batch will be cancelled only when _all_ items are cancelled\n */\nexport function dataLoader<TKey, TValue>(\n batchLoader: BatchLoader<TKey, TValue>,\n) {\n let pendingItems: BatchItem<TKey, TValue>[] | null = null;\n let dispatchTimer: ReturnType<typeof setTimeout> | null = null;\n\n const destroyTimerAndPendingItems = () => {\n clearTimeout(dispatchTimer as any);\n dispatchTimer = null;\n pendingItems = null;\n };\n\n /**\n * Iterate through the items and split them into groups based on the `batchLoader`'s validate function\n */\n function groupItems(items: BatchItem<TKey, TValue>[]) {\n const groupedItems: BatchItem<TKey, TValue>[][] = [[]];\n let index = 0;\n while (true) {\n const item = items[index];\n if (!item) {\n // we're done\n break;\n }\n const lastGroup = groupedItems[groupedItems.length - 1]!;\n\n if (item.aborted) {\n // Item was aborted before it was dispatched\n item.reject?.(new Error('Aborted'));\n index++;\n continue;\n }\n\n const isValid = batchLoader.validate(\n lastGroup.concat(item).map((it) => it.key),\n );\n\n if (isValid) {\n lastGroup.push(item);\n index++;\n continue;\n }\n\n if (lastGroup.length === 0) {\n item.reject?.(new Error('Input is too big for a single dispatch'));\n index++;\n continue;\n }\n // Create new group, next iteration will try to add the item to that\n groupedItems.push([]);\n }\n return groupedItems;\n }\n\n function dispatch() {\n const groupedItems = groupItems(pendingItems!);\n destroyTimerAndPendingItems();\n\n // Create batches for each group of items\n for (const items of groupedItems) {\n if (!items.length) {\n continue;\n }\n const batch: Batch<TKey, TValue> = {\n items,\n };\n for (const item of items) {\n item.batch = batch;\n }\n const promise = batchLoader.fetch(batch.items.map((_item) => _item.key));\n\n promise\n .then(async (result) => {\n await Promise.all(\n result.map(async (valueOrPromise, index) => {\n const item = batch.items[index]!;\n try {\n const value = await Promise.resolve(valueOrPromise);\n\n item.resolve?.(value);\n } catch (cause) {\n item.reject?.(cause as Error);\n }\n\n item.batch = null;\n item.reject = null;\n item.resolve = null;\n }),\n );\n\n for (const item of batch.items) {\n item.reject?.(new Error('Missing result'));\n item.batch = null;\n }\n })\n .catch((cause) => {\n for (const item of batch.items) {\n item.reject?.(cause);\n item.batch = null;\n }\n });\n }\n }\n function load(key: TKey): Promise<TValue> {\n const item: BatchItem<TKey, TValue> = {\n aborted: false,\n key,\n batch: null,\n resolve: throwFatalError,\n reject: throwFatalError,\n };\n\n const promise = new Promise<TValue>((resolve, reject) => {\n item.reject = reject;\n item.resolve = resolve;\n\n pendingItems ??= [];\n pendingItems.push(item);\n });\n\n dispatchTimer ??= setTimeout(dispatch);\n\n return promise;\n }\n\n return {\n load,\n };\n}\n","import type { Maybe } from '@trpc/server/unstable-core-do-not-import';\n\n/**\n * Like `Promise.all()` but for abort signals\n * - When all signals have been aborted, the merged signal will be aborted\n * - If one signal is `null`, no signal will be aborted\n */\nexport function allAbortSignals(...signals: Maybe<AbortSignal>[]): AbortSignal {\n const ac = new AbortController();\n\n const count = signals.length;\n\n let abortedCount = 0;\n\n const onAbort = () => {\n if (++abortedCount === count) {\n ac.abort();\n }\n };\n\n for (const signal of signals) {\n if (signal?.aborted) {\n onAbort();\n } else {\n signal?.addEventListener('abort', onAbort, {\n once: true,\n });\n }\n }\n\n return ac.signal;\n}\n\n/**\n * Like `Promise.race` but for abort signals\n *\n * Basically, a ponyfill for\n * [`AbortSignal.any`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static).\n */\nexport function raceAbortSignals(\n ...signals: Maybe<AbortSignal>[]\n): AbortSignal {\n const ac = new AbortController();\n\n for (const signal of signals) {\n if (signal?.aborted) {\n ac.abort();\n } else {\n signal?.addEventListener('abort', () => ac.abort(), { once: true });\n }\n }\n\n return ac.signal;\n}\n\nexport function abortSignalToPromise(signal: AbortSignal): Promise<never> {\n return new Promise((_, reject) => {\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n signal.addEventListener(\n 'abort',\n () => {\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n","import type { AnyRouter, ProcedureType } from '@trpc/server';\nimport { observable } from '@trpc/server/observable';\nimport { transformResult } from '@trpc/server/unstable-core-do-not-import';\nimport type { BatchLoader } from '../internals/dataLoader';\nimport { dataLoader } from '../internals/dataLoader';\nimport { allAbortSignals } from '../internals/signals';\nimport type { NonEmptyArray } from '../internals/types';\nimport { TRPCClientError } from '../TRPCClientError';\nimport type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions';\nimport type { HTTPResult } from './internals/httpUtils';\nimport {\n getUrl,\n jsonHttpRequester,\n resolveHTTPLinkOptions,\n} from './internals/httpUtils';\nimport type { Operation, TRPCLink } from './types';\n\n/**\n * @see https://trpc.io/docs/client/links/httpBatchLink\n */\nexport function httpBatchLink<TRouter extends AnyRouter>(\n opts: HTTPBatchLinkOptions<TRouter['_def']['_config']['$types']>,\n): TRPCLink<TRouter> {\n const resolvedOpts = resolveHTTPLinkOptions(opts);\n const maxURLLength = opts.maxURLLength ?? Infinity;\n const maxItems = opts.maxItems ?? Infinity;\n\n return () => {\n const batchLoader = (\n type: ProcedureType,\n ): BatchLoader<Operation, HTTPResult> => {\n return {\n validate(batchOps) {\n if (maxURLLength === Infinity && maxItems === Infinity) {\n // escape hatch for quick calcs\n return true;\n }\n if (batchOps.length > maxItems) {\n return false;\n }\n const path = batchOps.map((op) => op.path).join(',');\n const inputs = batchOps.map((op) => op.input);\n\n const url = getUrl({\n ...resolvedOpts,\n type,\n path,\n inputs,\n signal: null,\n });\n\n return url.length <= maxURLLength;\n },\n async fetch(batchOps) {\n const path = batchOps.map((op) => op.path).join(',');\n const inputs = batchOps.map((op) => op.input);\n const signal = allAbortSignals(...batchOps.map((op) => op.signal));\n\n const res = await jsonHttpRequester({\n ...resolvedOpts,\n path,\n inputs,\n type,\n headers() {\n if (!opts.headers) {\n return {};\n }\n if (typeof opts.headers === 'function') {\n return opts.headers({\n opList: batchOps as NonEmptyArray<Operation>,\n });\n }\n return opts.headers;\n },\n signal,\n });\n const resJSON = Array.isArray(res.json)\n ? res.json\n : batchOps.map(() => res.json);\n const result = resJSON.map((item) => ({\n meta: res.meta,\n json: item,\n }));\n return result;\n },\n };\n };\n\n const query = dataLoader(batchLoader('query'));\n const mutation = dataLoader(batchLoader('mutation'));\n\n const loaders = { query, mutation };\n return ({ op }) => {\n return observable((observer) => {\n /* istanbul ignore if -- @preserve */\n if (op.type === 'subscription') {\n throw new Error(\n 'Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`',\n );\n }\n const loader = loaders[op.type];\n const promise = loader.load(op);\n\n let _res = undefined as HTTPResult | undefined;\n promise\n .then((res) => {\n _res = res;\n const transformed = transformResult(\n res.json,\n resolvedOpts.transformer.output,\n );\n\n if (!transformed.ok) {\n observer.error(\n TRPCClientError.from(transformed.error, {\n meta: res.meta,\n }),\n );\n return;\n }\n observer.next({\n context: res.meta,\n result: transformed.result,\n });\n observer.complete();\n })\n .catch((err) => {\n observer.error(\n TRPCClientError.from(err, {\n meta: _res?.meta,\n }),\n );\n });\n\n return () => {\n // noop\n };\n });\n };\n };\n}\n"],"mappings":"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"} |
| import { __toESM, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs"; | ||
| import { TRPCClientError } from "./TRPCClientError-apv8gw59.mjs"; | ||
| import { getUrl, httpRequest, jsonHttpRequester, resolveHTTPLinkOptions } from "./httpUtils-pyf5RF99.mjs"; | ||
| import { observable } from "@trpc/server/observable"; | ||
| import { transformResult } from "@trpc/server/unstable-core-do-not-import"; | ||
| //#region src/links/internals/contentTypes.ts | ||
| function isOctetType(input) { | ||
| return input instanceof Uint8Array || input instanceof Blob; | ||
| } | ||
| function isFormData(input) { | ||
| return input instanceof FormData; | ||
| } | ||
| function isNonJsonSerializable(input) { | ||
| return isOctetType(input) || isFormData(input); | ||
| } | ||
| //#endregion | ||
| //#region src/links/httpLink.ts | ||
| var import_objectSpread2 = __toESM(require_objectSpread2(), 1); | ||
| const universalRequester = (opts) => { | ||
| if ("input" in opts) { | ||
| const { input } = opts; | ||
| if (isFormData(input)) { | ||
| if (opts.type !== "mutation" && opts.methodOverride !== "POST") throw new Error("FormData is only supported for mutations"); | ||
| return httpRequest((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts), {}, { | ||
| contentTypeHeader: void 0, | ||
| 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((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts), {}, { | ||
| contentTypeHeader: "application/octet-stream", | ||
| getUrl, | ||
| getBody: () => input | ||
| })); | ||
| } | ||
| } | ||
| return jsonHttpRequester(opts); | ||
| }; | ||
| /** | ||
| * @see https://trpc.io/docs/client/links/httpLink | ||
| */ | ||
| function httpLink(opts) { | ||
| const resolvedOpts = resolveHTTPLinkOptions(opts); | ||
| return () => { | ||
| return (operationOpts) => { | ||
| const { op } = operationOpts; | ||
| 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((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, resolvedOpts), {}, { | ||
| type, | ||
| path, | ||
| input, | ||
| signal: op.signal, | ||
| headers() { | ||
| if (!opts.headers) return {}; | ||
| if (typeof opts.headers === "function") return opts.headers({ op }); | ||
| return opts.headers; | ||
| } | ||
| })); | ||
| let meta = void 0; | ||
| 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 () => {}; | ||
| }); | ||
| }; | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { httpLink, isFormData, isNonJsonSerializable, isOctetType }; | ||
| //# sourceMappingURL=httpLink-lG_6juPY.mjs.map |
| {"version":3,"file":"httpLink-lG_6juPY.mjs","names":["input: unknown","universalRequester: Requester","opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>","meta: HTTPResult['meta'] | undefined"],"sources":["../src/links/internals/contentTypes.ts","../src/links/httpLink.ts"],"sourcesContent":["export function isOctetType(\n input: unknown,\n): input is Uint8Array<ArrayBuffer> | Blob {\n return (\n input instanceof Uint8Array ||\n // File extends from Blob but is only available in nodejs from v20\n input instanceof Blob\n );\n}\n\nexport function isFormData(input: unknown) {\n return input instanceof FormData;\n}\n\nexport function isNonJsonSerializable(input: unknown) {\n return isOctetType(input) || isFormData(input);\n}\n","import { observable } from '@trpc/server/observable';\nimport type {\n AnyClientTypes,\n AnyRouter,\n} from '@trpc/server/unstable-core-do-not-import';\nimport { transformResult } from '@trpc/server/unstable-core-do-not-import';\nimport { TRPCClientError } from '../TRPCClientError';\nimport type {\n HTTPLinkBaseOptions,\n HTTPResult,\n Requester,\n} from './internals/httpUtils';\nimport {\n getUrl,\n httpRequest,\n jsonHttpRequester,\n resolveHTTPLinkOptions,\n} from './internals/httpUtils';\nimport {\n isFormData,\n isOctetType,\n type HTTPHeaders,\n type Operation,\n type TRPCLink,\n} from './types';\n\nexport type HTTPLinkOptions<TRoot extends AnyClientTypes> =\n HTTPLinkBaseOptions<TRoot> & {\n /**\n * Headers to be set on outgoing requests or a callback that of said headers\n * @see http://trpc.io/docs/client/headers\n */\n headers?:\n | HTTPHeaders\n | ((opts: { op: Operation }) => HTTPHeaders | Promise<HTTPHeaders>);\n };\n\nconst universalRequester: Requester = (opts) => {\n if ('input' in opts) {\n const { input } = opts;\n if (isFormData(input)) {\n if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') {\n throw new Error('FormData is only supported for mutations');\n }\n\n return httpRequest({\n ...opts,\n // The browser will set this automatically and include the boundary= in it\n contentTypeHeader: undefined,\n getUrl,\n getBody: () => input,\n });\n }\n\n if (isOctetType(input)) {\n if (opts.type !== 'mutation' && opts.methodOverride !== 'POST') {\n throw new Error('Octet type input is only supported for mutations');\n }\n\n return httpRequest({\n ...opts,\n contentTypeHeader: 'application/octet-stream',\n getUrl,\n getBody: () => input,\n });\n }\n }\n\n return jsonHttpRequester(opts);\n};\n\n/**\n * @see https://trpc.io/docs/client/links/httpLink\n */\nexport function httpLink<TRouter extends AnyRouter = AnyRouter>(\n opts: HTTPLinkOptions<TRouter['_def']['_config']['$types']>,\n): TRPCLink<TRouter> {\n const resolvedOpts = resolveHTTPLinkOptions(opts);\n return () => {\n return (operationOpts) => {\n const { op } = operationOpts;\n return observable((observer) => {\n const { path, input, type } = op;\n /* istanbul ignore if -- @preserve */\n if (type === 'subscription') {\n throw new Error(\n 'Subscriptions are unsupported by `httpLink` - use `httpSubscriptionLink` or `wsLink`',\n );\n }\n\n const request = universalRequester({\n ...resolvedOpts,\n type,\n path,\n input,\n signal: op.signal,\n headers() {\n if (!opts.headers) {\n return {};\n }\n if (typeof opts.headers === 'function') {\n return opts.headers({\n op,\n });\n }\n return opts.headers;\n },\n });\n let meta: HTTPResult['meta'] | undefined = undefined;\n request\n .then((res) => {\n meta = res.meta;\n const transformed = transformResult(\n res.json,\n resolvedOpts.transformer.output,\n );\n\n if (!transformed.ok) {\n observer.error(\n TRPCClientError.from(transformed.error, {\n meta,\n }),\n );\n return;\n }\n observer.next({\n context: res.meta,\n result: transformed.result,\n });\n observer.complete();\n })\n .catch((cause) => {\n observer.error(TRPCClientError.from(cause, { meta }));\n });\n\n return () => {\n // noop\n };\n });\n };\n };\n}\n"],"mappings":";;;;;;;AAAA,SAAgB,YACdA,OACyC;AACzC,QACE,iBAAiB,cAEjB,iBAAiB;AAEpB;AAED,SAAgB,WAAWA,OAAgB;AACzC,QAAO,iBAAiB;AACzB;AAED,SAAgB,sBAAsBA,OAAgB;AACpD,QAAO,YAAY,MAAM,IAAI,WAAW,MAAM;AAC/C;;;;;ACqBD,MAAMC,qBAAgC,CAAC,SAAS;AAC9C,KAAI,WAAW,MAAM;EACnB,MAAM,EAAE,OAAO,GAAG;AAClB,MAAI,WAAW,MAAM,EAAE;AACrB,OAAI,KAAK,SAAS,cAAc,KAAK,mBAAmB,OACtD,OAAM,IAAI,MAAM;AAGlB,UAAO,oFACF;IAEH;IACA;IACA,SAAS,MAAM;MACf;EACH;AAED,MAAI,YAAY,MAAM,EAAE;AACtB,OAAI,KAAK,SAAS,cAAc,KAAK,mBAAmB,OACtD,OAAM,IAAI,MAAM;AAGlB,UAAO,oFACF;IACH,mBAAmB;IACnB;IACA,SAAS,MAAM;MACf;EACH;CACF;AAED,QAAO,kBAAkB,KAAK;AAC/B;;;;AAKD,SAAgB,SACdC,MACmB;CACnB,MAAM,eAAe,uBAAuB,KAAK;AACjD,QAAO,MAAM;AACX,SAAO,CAAC,kBAAkB;GACxB,MAAM,EAAE,IAAI,GAAG;AACf,UAAO,WAAW,CAAC,aAAa;IAC9B,MAAM,EAAE,MAAM,OAAO,MAAM,GAAG;;AAE9B,QAAI,SAAS,eACX,OAAM,IAAI,MACR;IAIJ,MAAM,UAAU,2FACX;KACH;KACA;KACA;KACA,QAAQ,GAAG;KACX,UAAU;AACR,WAAK,KAAK,QACR,QAAO,CAAE;AAEX,iBAAW,KAAK,YAAY,WAC1B,QAAO,KAAK,QAAQ,EAClB,GACD,EAAC;AAEJ,aAAO,KAAK;KACb;OACD;IACF,IAAIC;AACJ,YACG,KAAK,CAAC,QAAQ;AACb,YAAO,IAAI;KACX,MAAM,cAAc,gBAClB,IAAI,MACJ,aAAa,YAAY,OAC1B;AAED,UAAK,YAAY,IAAI;AACnB,eAAS,MACP,gBAAgB,KAAK,YAAY,OAAO,EACtC,KACD,EAAC,CACH;AACD;KACD;AACD,cAAS,KAAK;MACZ,SAAS,IAAI;MACb,QAAQ,YAAY;KACrB,EAAC;AACF,cAAS,UAAU;IACpB,EAAC,CACD,MAAM,CAAC,UAAU;AAChB,cAAS,MAAM,gBAAgB,KAAK,OAAO,EAAE,KAAM,EAAC,CAAC;IACtD,EAAC;AAEJ,WAAO,MAAM,CAEZ;GACF,EAAC;EACH;CACF;AACF"} |
| import { __toESM, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs"; | ||
| import { getTransformer } from "./unstable-internals-Bg7n9BBj.mjs"; | ||
| //#region src/getFetch.ts | ||
| const isFunction = (fn) => typeof fn === "function"; | ||
| function getFetch(customFetchImpl) { | ||
| if (customFetchImpl) return customFetchImpl; | ||
| if (typeof window !== "undefined" && isFunction(window.fetch)) return window.fetch; | ||
| if (typeof globalThis !== "undefined" && isFunction(globalThis.fetch)) return globalThis.fetch; | ||
| throw new Error("No fetch implementation found"); | ||
| } | ||
| //#endregion | ||
| //#region src/links/internals/httpUtils.ts | ||
| var import_objectSpread2 = __toESM(require_objectSpread2(), 1); | ||
| function resolveHTTPLinkOptions(opts) { | ||
| return { | ||
| url: opts.url.toString(), | ||
| fetch: opts.fetch, | ||
| transformer: getTransformer(opts.transformer), | ||
| methodOverride: opts.methodOverride | ||
| }; | ||
| } | ||
| function arrayToDict(array) { | ||
| const dict = {}; | ||
| for (let index = 0; index < array.length; index++) { | ||
| const element = array[index]; | ||
| dict[index] = element; | ||
| } | ||
| return dict; | ||
| } | ||
| const METHOD = { | ||
| query: "GET", | ||
| mutation: "POST", | ||
| subscription: "PATCH" | ||
| }; | ||
| function getInput(opts) { | ||
| return "input" in opts ? opts.transformer.input.serialize(opts.input) : arrayToDict(opts.inputs.map((_input) => opts.transformer.input.serialize(_input))); | ||
| } | ||
| const getUrl = (opts) => { | ||
| const parts = opts.url.split("?"); | ||
| const base = parts[0].replace(/\/$/, ""); | ||
| 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" || opts.type === "subscription") { | ||
| const input = getInput(opts); | ||
| if (input !== void 0 && opts.methodOverride !== "POST") queryParts.push(`input=${encodeURIComponent(JSON.stringify(input))}`); | ||
| } | ||
| if (queryParts.length) url += "?" + queryParts.join("&"); | ||
| return url; | ||
| }; | ||
| const getBody = (opts) => { | ||
| if (opts.type === "query" && opts.methodOverride !== "POST") return void 0; | ||
| const input = getInput(opts); | ||
| return input !== void 0 ? JSON.stringify(input) : void 0; | ||
| }; | ||
| const jsonHttpRequester = (opts) => { | ||
| return httpRequest((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts), {}, { | ||
| contentTypeHeader: "application/json", | ||
| getUrl, | ||
| getBody | ||
| })); | ||
| }; | ||
| /** | ||
| * Polyfill for DOMException with AbortError name | ||
| */ | ||
| var AbortError = class 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) => { | ||
| var _signal$throwIfAborte; | ||
| if (!(signal === null || signal === void 0 ? void 0 : signal.aborted)) return; | ||
| (_signal$throwIfAborte = signal.throwIfAborted) === null || _signal$throwIfAborte === void 0 || _signal$throwIfAborte.call(signal); | ||
| if (typeof DOMException !== "undefined") throw new DOMException("AbortError", "AbortError"); | ||
| throw new AbortError(); | ||
| }; | ||
| async function fetchHTTPResponse(opts) { | ||
| var _opts$methodOverride, _opts$trpcAcceptHeade; | ||
| throwIfAborted(opts.signal); | ||
| const url = opts.getUrl(opts); | ||
| const body = opts.getBody(opts); | ||
| const method = (_opts$methodOverride = opts.methodOverride) !== null && _opts$methodOverride !== void 0 ? _opts$methodOverride : METHOD[opts.type]; | ||
| const resolvedHeaders = await (async () => { | ||
| const heads = await opts.headers(); | ||
| if (Symbol.iterator in heads) return Object.fromEntries(heads); | ||
| return heads; | ||
| })(); | ||
| const headers = (0, import_objectSpread2.default)((0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, opts.contentTypeHeader && method !== "GET" ? { "content-type": opts.contentTypeHeader } : {}), opts.trpcAcceptHeader ? { [(_opts$trpcAcceptHeade = opts.trpcAcceptHeaderKey) !== null && _opts$trpcAcceptHeade !== void 0 ? _opts$trpcAcceptHeade : "trpc-accept"]: opts.trpcAcceptHeader } : void 0), resolvedHeaders); | ||
| return getFetch(opts.fetch)(url, { | ||
| method, | ||
| signal: opts.signal, | ||
| body, | ||
| headers | ||
| }); | ||
| } | ||
| async function httpRequest(opts) { | ||
| const meta = {}; | ||
| const res = await fetchHTTPResponse(opts); | ||
| meta.response = res; | ||
| const json = await res.json(); | ||
| meta.responseJSON = json; | ||
| return { | ||
| json, | ||
| meta | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { fetchHTTPResponse, getBody, getFetch, getUrl, httpRequest, jsonHttpRequester, resolveHTTPLinkOptions }; | ||
| //# sourceMappingURL=httpUtils-pyf5RF99.mjs.map |
| {"version":3,"file":"httpUtils-pyf5RF99.mjs","names":["fn: unknown","customFetchImpl?: FetchEsque | NativeFetchEsque","opts: HTTPLinkBaseOptions<AnyClientTypes>","array: unknown[]","dict: Record<number, unknown>","opts: GetInputOptions","getUrl: GetUrl","queryParts: string[]","getBody: GetBody","jsonHttpRequester: Requester","signal: Maybe<AbortSignal>","opts: HTTPRequestOptions"],"sources":["../src/getFetch.ts","../src/links/internals/httpUtils.ts"],"sourcesContent":["import type { FetchEsque, NativeFetchEsque } from './internals/types';\n\ntype AnyFn = (...args: any[]) => unknown;\n\nconst isFunction = (fn: unknown): fn is AnyFn => typeof fn === 'function';\n\nexport function getFetch(\n customFetchImpl?: FetchEsque | NativeFetchEsque,\n): FetchEsque {\n if (customFetchImpl) {\n return customFetchImpl as FetchEsque;\n }\n\n if (typeof window !== 'undefined' && isFunction(window.fetch)) {\n return window.fetch as FetchEsque;\n }\n\n if (typeof globalThis !== 'undefined' && isFunction(globalThis.fetch)) {\n return globalThis.fetch as FetchEsque;\n }\n\n throw new Error('No fetch implementation found');\n}\n","import type {\n AnyClientTypes,\n CombinedDataTransformer,\n Maybe,\n ProcedureType,\n TRPCAcceptHeader,\n TRPCResponse,\n} from '@trpc/server/unstable-core-do-not-import';\nimport { getFetch } from '../../getFetch';\nimport type {\n FetchEsque,\n RequestInitEsque,\n ResponseEsque,\n} from '../../internals/types';\nimport type { TransformerOptions } from '../../unstable-internals';\nimport { getTransformer } from '../../unstable-internals';\nimport type { HTTPHeaders } from '../types';\n\n/**\n * @internal\n */\nexport type HTTPLinkBaseOptions<\n TRoot extends Pick<AnyClientTypes, 'transformer'>,\n> = {\n url: string | URL;\n /**\n * Add ponyfill for fetch\n */\n fetch?: FetchEsque;\n /**\n * Send all requests `as POST`s requests regardless of the procedure type\n * The HTTP handler must separately allow overriding the method. See:\n * @see https://trpc.io/docs/rpc\n */\n methodOverride?: 'POST';\n} & TransformerOptions<TRoot>;\n\nexport interface ResolvedHTTPLinkOptions {\n url: string;\n fetch?: FetchEsque;\n transformer: CombinedDataTransformer;\n methodOverride?: 'POST';\n}\n\nexport function resolveHTTPLinkOptions(\n opts: HTTPLinkBaseOptions<AnyClientTypes>,\n): ResolvedHTTPLinkOptions {\n return {\n url: opts.url.toString(),\n fetch: opts.fetch,\n transformer: getTransformer(opts.transformer),\n methodOverride: opts.methodOverride,\n };\n}\n\n// https://github.com/trpc/trpc/pull/669\nfunction arrayToDict(array: unknown[]) {\n const dict: Record<number, unknown> = {};\n for (let index = 0; index < array.length; index++) {\n const element = array[index];\n dict[index] = element;\n }\n return dict;\n}\n\nconst METHOD = {\n query: 'GET',\n mutation: 'POST',\n subscription: 'PATCH',\n} as const;\n\nexport interface HTTPResult {\n json: TRPCResponse;\n meta: {\n response: ResponseEsque;\n responseJSON?: unknown;\n };\n}\n\ntype GetInputOptions = {\n transformer: CombinedDataTransformer;\n} & ({ input: unknown } | { inputs: unknown[] });\n\nexport function getInput(opts: GetInputOptions) {\n return 'input' in opts\n ? opts.transformer.input.serialize(opts.input)\n : arrayToDict(\n opts.inputs.map((_input) => opts.transformer.input.serialize(_input)),\n );\n}\n\nexport type HTTPBaseRequestOptions = GetInputOptions &\n ResolvedHTTPLinkOptions & {\n type: ProcedureType;\n path: string;\n signal: Maybe<AbortSignal>;\n };\n\ntype GetUrl = (opts: HTTPBaseRequestOptions) => string;\ntype GetBody = (opts: HTTPBaseRequestOptions) => RequestInitEsque['body'];\n\nexport type ContentOptions = {\n trpcAcceptHeader?: TRPCAcceptHeader;\n trpcAcceptHeaderKey?: 'trpc-accept' | 'accept';\n contentTypeHeader?: string;\n getUrl: GetUrl;\n getBody: GetBody;\n};\n\nexport const getUrl: GetUrl = (opts) => {\n const parts = opts.url.split('?') as [string, string?];\n const base = parts[0].replace(/\\/$/, ''); // Remove any trailing slashes\n\n let url = base + '/' + opts.path;\n const queryParts: string[] = [];\n\n if (parts[1]) {\n queryParts.push(parts[1]);\n }\n if ('inputs' in opts) {\n queryParts.push('batch=1');\n }\n if (opts.type === 'query' || opts.type === 'subscription') {\n const input = getInput(opts);\n if (input !== undefined && opts.methodOverride !== 'POST') {\n queryParts.push(`input=${encodeURIComponent(JSON.stringify(input))}`);\n }\n }\n if (queryParts.length) {\n url += '?' + queryParts.join('&');\n }\n return url;\n};\n\nexport const getBody: GetBody = (opts) => {\n if (opts.type === 'query' && opts.methodOverride !== 'POST') {\n return undefined;\n }\n const input = getInput(opts);\n return input !== undefined ? JSON.stringify(input) : undefined;\n};\n\nexport type Requester = (\n opts: HTTPBaseRequestOptions & {\n headers: () => HTTPHeaders | Promise<HTTPHeaders>;\n },\n) => Promise<HTTPResult>;\n\nexport const jsonHttpRequester: Requester = (opts) => {\n return httpRequest({\n ...opts,\n contentTypeHeader: 'application/json',\n getUrl,\n getBody,\n });\n};\n\n/**\n * Polyfill for DOMException with AbortError name\n */\nclass AbortError extends Error {\n constructor() {\n const name = 'AbortError';\n super(name);\n this.name = name;\n this.message = name;\n }\n}\n\nexport type HTTPRequestOptions = ContentOptions &\n HTTPBaseRequestOptions & {\n headers: () => HTTPHeaders | Promise<HTTPHeaders>;\n };\n\n/**\n * Polyfill for `signal.throwIfAborted()`\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted\n */\nconst throwIfAborted = (signal: Maybe<AbortSignal>) => {\n if (!signal?.aborted) {\n return;\n }\n // If available, use the native implementation\n signal.throwIfAborted?.();\n\n // If we have `DOMException`, use it\n if (typeof DOMException !== 'undefined') {\n throw new DOMException('AbortError', 'AbortError');\n }\n\n // Otherwise, use our own implementation\n throw new AbortError();\n};\n\nexport async function fetchHTTPResponse(opts: HTTPRequestOptions) {\n throwIfAborted(opts.signal);\n\n const url = opts.getUrl(opts);\n const body = opts.getBody(opts);\n const method = opts.methodOverride ?? METHOD[opts.type];\n const resolvedHeaders = await (async () => {\n const heads = await opts.headers();\n if (Symbol.iterator in heads) {\n return Object.fromEntries(heads);\n }\n return heads;\n })();\n const headers = {\n ...(opts.contentTypeHeader && method !== 'GET'\n ? { 'content-type': opts.contentTypeHeader }\n : {}),\n ...(opts.trpcAcceptHeader\n ? { [opts.trpcAcceptHeaderKey ?? 'trpc-accept']: opts.trpcAcceptHeader }\n : undefined),\n ...resolvedHeaders,\n };\n\n return getFetch(opts.fetch)(url, {\n method,\n signal: opts.signal,\n body,\n headers,\n });\n}\n\nexport async function httpRequest(\n opts: HTTPRequestOptions,\n): Promise<HTTPResult> {\n const meta = {} as HTTPResult['meta'];\n\n const res = await fetchHTTPResponse(opts);\n meta.response = res;\n\n const json = await res.json();\n\n meta.responseJSON = json;\n\n return {\n json: json as TRPCResponse,\n meta,\n };\n}\n"],"mappings":"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"} |
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
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
605628
6.64%146
3.55%8649
0.23%74
12.12%13
8.33%1
Infinity%63
1.61%