FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

27
node_modules/@trpc/server/src/@trpc/server/http.ts generated vendored Normal file
View File

@@ -0,0 +1,27 @@
export {
getHTTPStatusCode,
getHTTPStatusCodeFromError,
resolveResponse,
} from '../../unstable-core-do-not-import';
export type {
BaseHandlerOptions,
HTTPBaseHandlerOptions,
HTTPErrorHandler,
/**
* @deprecated Use `HTTPErrorHandler` instead
*/
HTTPErrorHandler as OnErrorFunction,
ResolveHTTPRequestOptionsContextFn,
ResponseMeta,
ResponseMetaFn,
TRPCRequestInfo,
OctetInput,
FileLike,
UtilityParser,
} from '../../unstable-core-do-not-import';
export {
octetInputParser,
parseConnectionParamsFromUnknown,
parseConnectionParamsFromString,
} from '../../unstable-core-do-not-import';

145
node_modules/@trpc/server/src/@trpc/server/index.ts generated vendored Normal file
View File

@@ -0,0 +1,145 @@
export {
TRPCError,
/**
* @deprecated use `experimental_trpcMiddleware` instead
*/
experimental_standaloneMiddleware,
experimental_standaloneMiddleware as experimental_trpcMiddleware,
initTRPC,
// --- FIXME a bunch of these exports are only useful for plugins - move them somewhere else? ----
getTRPCErrorFromUnknown,
transformTRPCResponse,
createFlatProxy as createTRPCFlatProxy,
createRecursiveProxy as createTRPCRecursiveProxy,
type inferProcedureInput,
type inferProcedureOutput,
type inferProcedureBuilderResolverOptions,
type inferRouterError,
type inferRouterInputs,
type inferRouterOutputs,
type inferRouterContext,
type inferClientTypes as inferTRPCClientTypes,
type AnyClientTypes as AnyTRPCClientTypes,
type inferTransformedProcedureOutput,
type inferTransformedSubscriptionOutput,
type AnyProcedure as AnyTRPCProcedure,
type AnyRouter as AnyTRPCRouter,
type RouterDef as TRPCRouterDef,
type RouterBuilder as TRPCRouterBuilder,
type RouterCallerFactory as TRPCRouterCallerFactory,
type RootConfig as TRPCRootConfig,
type AnyRootTypes as AnyTRPCRootTypes,
type MiddlewareFunction as TRPCMiddlewareFunction,
type MiddlewareBuilder as TRPCMiddlewareBuilder,
type AnyMiddlewareFunction as AnyTRPCMiddlewareFunction,
type CombinedDataTransformer as TRPCCombinedDataTransformer,
type DataTransformer as TRPCDataTransformer,
type ProcedureType as TRPCProcedureType,
type AnyMutationProcedure as AnyTRPCMutationProcedure,
type AnyQueryProcedure as AnyTRPCQueryProcedure,
type RouterRecord as TRPCRouterRecord,
type MergeRouters as TRPCMergeRouters,
type AnySubscriptionProcedure as AnyTRPCSubscriptionProcedure,
type CreateContextCallback,
type MutationProcedure as TRPCMutationProcedure,
type QueryProcedure as TRPCQueryProcedure,
type BuiltRouter as TRPCBuiltRouter,
type SubscriptionProcedure as TRPCSubscriptionProcedure,
type TRPCBuilder,
type ProcedureBuilder as TRPCProcedureBuilder,
type RuntimeConfigOptions as TRPCRuntimeConfigOptions,
type TRPCRootObject,
type ErrorFormatter as TRPCErrorFormatter,
type TRPCErrorShape,
type DefaultErrorShape as TRPCDefaultErrorShape,
type DefaultErrorData as TRPCDefaultErrorData,
type TRPC_ERROR_CODE_KEY,
type TRPC_ERROR_CODE_NUMBER,
type DecorateCreateRouterOptions as TRPCDecorateCreateRouterOptions,
type CreateRouterOptions as TRPCCreateRouterOptions,
type RouterCaller as TRPCRouterCaller,
StandardSchemaV1Error,
/**
* @deprecated use `tracked(id, data)` instead
*/
sse,
tracked,
type TrackedEnvelope,
isTrackedEnvelope,
lazy,
/**
* @deprecated use {@link lazy} instead
*/
lazy as experimental_lazy,
callProcedure as callTRPCProcedure,
/**
* @internal
*/
type UnsetMarker as TRPCUnsetMarker,
} from '../../unstable-core-do-not-import';
export type {
/**
* @deprecated use `AnyTRPCProcedure` instead
*/
AnyProcedure,
/**
* @deprecated use `AnyTRPCRouter` instead
*/
AnyRouter,
/**
* @deprecated use `AnyTRPCMiddlewareFunction` instead
*/
AnyMiddlewareFunction,
/**
* @deprecated use `TRPCCombinedDataTransformer` instead
*/
CombinedDataTransformer,
/**
* @deprecated use `TRPCDataTransformer` instead
*/
DataTransformer,
/**
* @deprecated This is a utility type will be removed in v12
*/
Dict,
/**
* @deprecated This is a utility type will be removed in v12
*/
DeepPartial,
/**
* @deprecated use `TRPCProcedureType` instead
*/
ProcedureType,
/**
* @deprecated use `AnyTRPCMutationProcedure` instead
*/
AnyMutationProcedure,
/**
* @deprecated use `AnyTRPCQueryProcedure` instead
*/
AnyQueryProcedure,
/**
* @deprecated use `AnyTRPCSubscriptionProcedure` instead
*/
AnySubscriptionProcedure,
} from '../../unstable-core-do-not-import';
export {
/**
* @deprecated use `getTRPCErrorShape` instead
*/
getErrorShape,
getErrorShape as getTRPCErrorShape,
} from '../../unstable-core-do-not-import';
/**
* @deprecated
* Use `Awaited<ReturnType<typeof myFunction>>` instead
*/
export type inferAsyncReturnType<TFunction extends (...args: any[]) => any> =
Awaited<ReturnType<TFunction>>;

27
node_modules/@trpc/server/src/@trpc/server/rpc.ts generated vendored Normal file
View File

@@ -0,0 +1,27 @@
// Note: this should likely be moved to a sort of `@trpc/plugin` package
export type {
JSONRPC2,
TRPCClientIncomingMessage,
TRPCClientIncomingRequest,
TRPCClientOutgoingMessage,
TRPCClientOutgoingRequest,
TRPCErrorResponse,
TRPCErrorShape,
TRPCReconnectNotification,
TRPCRequest,
TRPCRequestMessage,
TRPCResponse,
TRPCResponseMessage,
TRPCResult,
TRPCResultMessage,
TRPCSubscriptionStopNotification,
TRPCSuccessResponse,
TRPC_ERROR_CODE_KEY,
TRPC_ERROR_CODE_NUMBER,
TRPCConnectionParamsMessage,
} from '../../unstable-core-do-not-import';
export {
TRPC_ERROR_CODES_BY_KEY,
TRPC_ERROR_CODES_BY_NUMBER,
parseTRPCMessage,
} from '../../unstable-core-do-not-import';

View File

@@ -0,0 +1,254 @@
import { Readable, type Writable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import type {
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
APIGatewayProxyStructuredResultV2,
} from 'aws-lambda';
import { splitSetCookieString } from '../../vendor/cookie-es/set-cookie/split';
export type LambdaEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
export type APIGatewayResult =
| APIGatewayProxyResult
| APIGatewayProxyStructuredResultV2;
function determinePayloadFormat(event: LambdaEvent): string {
// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
// According to AWS support, version is is extracted from the version property in the event.
// If there is no version property, then the version is implied as 1.0
const unknownEvent = event as { version?: string };
if (typeof unknownEvent.version === 'undefined') {
return '1.0';
} else {
return unknownEvent.version;
}
}
/** 1:1 mapping of v1 or v2 input events, deduces which is which.
* @internal
**/
export type inferAPIGWReturn<TEvent> = TEvent extends APIGatewayProxyEvent
? APIGatewayProxyResult
: TEvent extends APIGatewayProxyEventV2
? APIGatewayProxyStructuredResultV2
: never;
interface Processor<TEvent extends LambdaEvent> {
getTRPCPath: (event: TEvent) => string;
url(event: TEvent): Pick<URL, 'hostname' | 'pathname' | 'search'>;
getHeaders: (event: TEvent) => Headers;
getMethod: (event: TEvent) => string;
toResult: (response: Response) => Promise<inferAPIGWReturn<TEvent>>;
toStream: (response: Response, stream: Writable) => Promise<void>;
}
function getHeadersAndCookiesFromResponse(response: Response) {
const headers = Object.fromEntries(response.headers.entries());
const cookies: string[] = splitSetCookieString(
response.headers.getSetCookie(),
).map((cookie) => cookie.trim());
delete headers['set-cookie'];
return { headers, cookies };
}
const v1Processor: Processor<APIGatewayProxyEvent> = {
// same as getPath above
getTRPCPath: (event) => {
if (!event.pathParameters) {
// Then this event was not triggered by a resource denoted with {proxy+}
return event.path.split('/').pop() ?? '';
}
const matches = event.resource.matchAll(/\{(.*?)\}/g);
for (const match of matches) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const group = match[1]!;
if (group.includes('+') && event.pathParameters) {
return event.pathParameters[group.replace('+', '')] ?? '';
}
}
return event.path.slice(1);
},
url(event) {
const hostname: string =
event.requestContext.domainName ??
event.headers['host'] ??
event.multiValueHeaders?.['host']?.[0] ??
'localhost';
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(
event.queryStringParameters ?? {},
)) {
if (value !== undefined) {
searchParams.append(key, value);
}
}
const qs = searchParams.toString();
return {
hostname,
pathname: event.path,
search: qs && `?${qs}`,
};
},
getHeaders: (event) => {
const headers = new Headers();
// Process multiValueHeaders first (takes precedence per AWS docs)
// This handles headers that can have multiple values
for (const [k, values] of Object.entries(event.multiValueHeaders ?? {})) {
if (values) {
values.forEach((v) => headers.append(k, v));
}
}
// Then process single-value headers, but skip any that were already
// added from multiValueHeaders to avoid duplication
for (const [key, value] of Object.entries(event.headers ?? {})) {
if (value !== undefined && !headers.has(key)) {
headers.append(key, value);
}
}
return headers;
},
getMethod: (event) => event.httpMethod,
toResult: async (response) => {
const { headers, cookies } = getHeadersAndCookiesFromResponse(response);
const result: APIGatewayProxyResult = {
...(cookies.length && { multiValueHeaders: { 'set-cookie': cookies } }),
statusCode: response.status,
body: await response.text(),
headers,
};
return result;
},
toStream: async (response, stream) => {
const { headers, cookies } = getHeadersAndCookiesFromResponse(response);
const metadata = {
statusCode: response.status,
headers,
cookies,
};
const responseStream = awslambda.HttpResponseStream.from(stream, metadata);
if (response.body) {
await pipeline(Readable.fromWeb(response.body as any), responseStream);
} else {
responseStream.end();
}
},
};
const v2Processor: Processor<APIGatewayProxyEventV2> = {
getTRPCPath: (event) => {
const matches = event.routeKey.matchAll(/\{(.*?)\}/g);
for (const match of matches) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const group = match[1]!;
if (group.includes('+') && event.pathParameters) {
return event.pathParameters[group.replace('+', '')] ?? '';
}
}
return event.rawPath.slice(1);
},
url(event) {
return {
hostname: event.requestContext.domainName,
pathname: event.rawPath,
search: event.rawQueryString && `?${event.rawQueryString}`,
};
},
getHeaders: (event) => {
const headers = new Headers();
for (const [key, value] of Object.entries(event.headers ?? {})) {
if (value !== undefined) {
headers.append(key, value);
}
}
if (event.cookies) {
headers.append('cookie', event.cookies.join('; '));
}
return headers;
},
getMethod: (event) => event.requestContext.http.method,
toResult: async (response) => {
const { headers, cookies } = getHeadersAndCookiesFromResponse(response);
const result: APIGatewayProxyStructuredResultV2 = {
cookies,
statusCode: response.status,
body: await response.text(),
headers,
};
return result;
},
toStream: async (response, stream) => {
const { headers, cookies } = getHeadersAndCookiesFromResponse(response);
const metadata = {
statusCode: response.status,
headers,
cookies,
};
const responseStream = awslambda.HttpResponseStream.from(stream, metadata);
if (response.body) {
await pipeline(Readable.fromWeb(response.body as any), responseStream);
} else {
responseStream.end();
}
},
};
export function getPlanner<TEvent extends LambdaEvent>(event: TEvent) {
const version = determinePayloadFormat(event);
let processor: Processor<TEvent>;
switch (version) {
case '1.0':
processor = v1Processor as Processor<TEvent>;
break;
case '2.0':
processor = v2Processor as Processor<TEvent>;
break;
default:
throw new Error(`Unsupported version: ${version}`);
}
const urlParts = processor.url(event);
const url = `https://${urlParts.hostname}${urlParts.pathname}${urlParts.search}`;
const init: RequestInit = {
headers: processor.getHeaders(event),
method: processor.getMethod(event),
// @ts-expect-error this is fine
duplex: 'half',
};
if (event.body) {
init.body = event.isBase64Encoded
? Buffer.from(event.body, 'base64')
: event.body;
}
const request = new Request(url, init);
return {
path: processor.getTRPCPath(event),
request,
toResult: processor.toResult,
toStream: processor.toStream,
};
}

View File

@@ -0,0 +1,115 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
import type { Context as APIGWContext, StreamifyHandler } from 'aws-lambda';
// @trpc/server
import type {
AnyRouter,
CreateContextCallback,
inferRouterContext,
} from '../../@trpc/server';
// @trpc/server
import type {
HTTPBaseHandlerOptions,
ResolveHTTPRequestOptionsContextFn,
TRPCRequestInfo,
} from '../../@trpc/server/http';
import { resolveResponse } from '../../@trpc/server/http';
import type { inferAPIGWReturn, LambdaEvent } from './getPlanner';
import { getPlanner } from './getPlanner';
export type CreateAWSLambdaContextOptions<TEvent extends LambdaEvent> = {
event: TEvent;
context: APIGWContext;
info: TRPCRequestInfo;
};
export type AWSLambdaOptions<
TRouter extends AnyRouter,
TEvent extends LambdaEvent,
> = HTTPBaseHandlerOptions<TRouter, TEvent> &
CreateContextCallback<
inferRouterContext<AnyRouter>,
AWSLambdaCreateContextFn<TRouter, TEvent>
>;
export type AWSLambdaCreateContextFn<
TRouter extends AnyRouter,
TEvent extends LambdaEvent,
> = ({
event,
context,
info,
}: CreateAWSLambdaContextOptions<TEvent>) =>
| inferRouterContext<TRouter>
| Promise<inferRouterContext<TRouter>>;
export function awsLambdaRequestHandler<
TRouter extends AnyRouter,
TEvent extends LambdaEvent,
>(
opts: AWSLambdaOptions<TRouter, TEvent>,
): (event: TEvent, context: APIGWContext) => Promise<inferAPIGWReturn<TEvent>> {
return async (event, context) => {
const planner = getPlanner(event);
const createContext: ResolveHTTPRequestOptionsContextFn<TRouter> = async (
innerOpts,
) => {
return await opts.createContext?.({ event, context, ...innerOpts });
};
const response = await resolveResponse({
...opts,
createContext,
req: planner.request,
path: planner.path,
error: null,
onError(o) {
opts?.onError?.({
...o,
req: event,
});
},
});
return await planner.toResult(response);
};
}
export function awsLambdaStreamingRequestHandler<
TRouter extends AnyRouter,
TEvent extends LambdaEvent,
>(opts: AWSLambdaOptions<TRouter, TEvent>): StreamifyHandler<TEvent> {
return async (event, responseStream, context) => {
const planner = getPlanner(event);
const createContext: ResolveHTTPRequestOptionsContextFn<TRouter> = async (
innerOpts,
) => {
return await opts.createContext?.({ event, context, ...innerOpts });
};
const response = await resolveResponse({
...opts,
createContext,
req: planner.request,
path: planner.path,
error: null,
onError(o) {
opts?.onError?.({
...o,
req: event,
});
},
});
await planner.toStream(response, responseStream);
};
}

48
node_modules/@trpc/server/src/adapters/express.ts generated vendored Normal file
View File

@@ -0,0 +1,48 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
import type * as express from 'express';
import type { AnyRouter } from '../@trpc/server';
// eslint-disable-next-line no-restricted-imports
import { run } from '../unstable-core-do-not-import';
import type {
NodeHTTPCreateContextFnOptions,
NodeHTTPHandlerOptions,
} from './node-http';
import { internal_exceptionHandler, nodeHTTPRequestHandler } from './node-http';
export type CreateExpressContextOptions = NodeHTTPCreateContextFnOptions<
express.Request,
express.Response
>;
export function createExpressMiddleware<TRouter extends AnyRouter>(
opts: NodeHTTPHandlerOptions<TRouter, express.Request, express.Response>,
): express.Handler {
return (req, res) => {
let path = '';
run(async () => {
path = req.path.slice(req.path.lastIndexOf('/') + 1);
await nodeHTTPRequestHandler({
...(opts as any),
req,
res,
path,
});
}).catch(
internal_exceptionHandler({
req,
res,
path,
...opts,
}),
);
};
}

View File

@@ -0,0 +1,81 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
import type { FastifyReply, FastifyRequest } from 'fastify';
// @trpc/server
import type { AnyRouter } from '../../@trpc/server';
// @trpc/server/http
import {
resolveResponse,
type HTTPBaseHandlerOptions,
type ResolveHTTPRequestOptionsContextFn,
} from '../../@trpc/server/http';
// @trpc/server/node-http
import type { NodeHTTPRequest } from '../node-http';
import {
incomingMessageToRequest,
type NodeHTTPCreateContextOption,
} from '../node-http';
export type FastifyHandlerOptions<
TRouter extends AnyRouter,
TRequest extends FastifyRequest,
TResponse extends FastifyReply,
> = HTTPBaseHandlerOptions<TRouter, TRequest> &
NodeHTTPCreateContextOption<TRouter, TRequest, TResponse>;
type FastifyRequestHandlerOptions<
TRouter extends AnyRouter,
TRequest extends FastifyRequest,
TResponse extends FastifyReply,
> = FastifyHandlerOptions<TRouter, TRequest, TResponse> & {
req: TRequest;
res: TResponse;
path: string;
};
export async function fastifyRequestHandler<
TRouter extends AnyRouter,
TRequest extends FastifyRequest,
TResponse extends FastifyReply,
>(opts: FastifyRequestHandlerOptions<TRouter, TRequest, TResponse>) {
const createContext: ResolveHTTPRequestOptionsContextFn<TRouter> = async (
innerOpts,
) => {
return await opts.createContext?.({
...opts,
...innerOpts,
});
};
const incomingMessage: NodeHTTPRequest = opts.req.raw;
// monkey-path body to the IncomingMessage
if ('body' in opts.req) {
incomingMessage.body = opts.req.body;
}
const req = incomingMessageToRequest(incomingMessage, opts.res.raw, {
maxBodySize: null,
});
const res = await resolveResponse({
...opts,
req,
error: null,
createContext,
onError(o) {
opts?.onError?.({
...o,
req: opts.req,
});
},
});
await opts.res.send(res);
}

View File

@@ -0,0 +1,89 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
/// <reference types="@fastify/websocket" />
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
// @trpc/server
import type { AnyRouter } from '../../@trpc/server';
// @trpc/server/http
import type { NodeHTTPCreateContextFnOptions } from '../node-http';
// @trpc/server/ws
import {
getWSConnectionHandler,
handleKeepAlive,
type WSSHandlerOptions,
} from '../ws';
import type { FastifyHandlerOptions } from './fastifyRequestHandler';
import { fastifyRequestHandler } from './fastifyRequestHandler';
export interface FastifyTRPCPluginOptions<TRouter extends AnyRouter> {
prefix?: string;
useWSS?: boolean;
trpcOptions: FastifyHandlerOptions<TRouter, FastifyRequest, FastifyReply>;
}
export type CreateFastifyContextOptions = NodeHTTPCreateContextFnOptions<
FastifyRequest,
FastifyReply
>;
export function fastifyTRPCPlugin<TRouter extends AnyRouter>(
fastify: FastifyInstance,
opts: FastifyTRPCPluginOptions<TRouter>,
done: (err?: Error) => void,
) {
fastify.removeContentTypeParser('application/json');
fastify.addContentTypeParser(
'application/json',
{ parseAs: 'string' },
function (_, body, _done) {
_done(null, body);
},
);
fastify.removeContentTypeParser('multipart/form-data');
fastify.addContentTypeParser(
'multipart/form-data',
{},
function (_, body, _done) {
_done(null, body);
},
);
let prefix = opts.prefix ?? '';
// https://github.com/fastify/fastify-plugin/blob/fe079bef6557a83794bf437e14b9b9edb8a74104/plugin.js#L11
// @ts-expect-error property 'default' does not exists on type ...
if (typeof fastifyTRPCPlugin.default !== 'function') {
prefix = ''; // handled by fastify internally
}
fastify.all(`${prefix}/:path`, async (req, res) => {
const path = (req.params as any).path;
await fastifyRequestHandler({ ...opts.trpcOptions, req, res, path });
});
if (opts.useWSS) {
const trpcOptions =
opts.trpcOptions as unknown as WSSHandlerOptions<TRouter>;
const onConnection = getWSConnectionHandler<TRouter>({
...trpcOptions,
});
fastify.get(prefix ?? '/', { websocket: true }, (socket, req) => {
onConnection(socket, req.raw);
if (trpcOptions?.keepAlive?.enabled) {
const { pingMs, pongWaitMs } = trpcOptions.keepAlive;
handleKeepAlive(socket, pingMs, pongWaitMs);
}
});
}
done();
}

View File

@@ -0,0 +1,2 @@
export * from './fastifyRequestHandler';
export * from './fastifyTRPCPlugin';

View File

@@ -0,0 +1,80 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
// @trpc/server
import type { AnyRouter } from '../../@trpc/server';
import type { ResolveHTTPRequestOptionsContextFn } from '../../@trpc/server/http';
import { resolveResponse } from '../../@trpc/server/http';
import type { FetchHandlerRequestOptions } from './types';
const trimSlashes = (path: string): string => {
path = path.startsWith('/') ? path.slice(1) : path;
path = path.endsWith('/') ? path.slice(0, -1) : path;
return path;
};
export async function fetchRequestHandler<TRouter extends AnyRouter>(
opts: FetchHandlerRequestOptions<TRouter>,
): Promise<Response> {
const resHeaders = new Headers();
const createContext: ResolveHTTPRequestOptionsContextFn<TRouter> = async (
innerOpts,
) => {
return opts.createContext?.({ req: opts.req, resHeaders, ...innerOpts });
};
const url = new URL(opts.req.url);
const pathname = trimSlashes(url.pathname);
const endpoint = trimSlashes(opts.endpoint);
const path = trimSlashes(pathname.slice(endpoint.length));
return await resolveResponse({
...opts,
req: opts.req,
createContext,
path,
error: null,
onError(o) {
opts?.onError?.({ ...o, req: opts.req });
},
responseMeta(data) {
const meta = opts.responseMeta?.(data);
if (meta?.headers) {
if (meta.headers instanceof Headers) {
for (const [key, value] of meta.headers.entries()) {
resHeaders.append(key, value);
}
} else {
/**
* @deprecated, delete in v12
*/
for (const [key, value] of Object.entries(meta.headers)) {
if (Array.isArray(value)) {
for (const v of value) {
resHeaders.append(key, v);
}
} else if (typeof value === 'string') {
resHeaders.set(key, value);
}
}
}
}
return {
headers: resHeaders,
status: meta?.status,
};
},
});
}

View File

@@ -0,0 +1,2 @@
export * from './fetchRequestHandler';
export * from './types';

53
node_modules/@trpc/server/src/adapters/fetch/types.ts generated vendored Normal file
View File

@@ -0,0 +1,53 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
// @trpc/server
import type {
AnyRouter,
CreateContextCallback,
inferRouterContext,
} from '../../@trpc/server';
// @trpc/server/http
import type {
HTTPBaseHandlerOptions,
TRPCRequestInfo,
} from '../../@trpc/server/http';
export type FetchCreateContextFnOptions = {
req: Request;
resHeaders: Headers;
info: TRPCRequestInfo;
};
export type FetchCreateContextFn<TRouter extends AnyRouter> = (
opts: FetchCreateContextFnOptions,
) => inferRouterContext<TRouter> | Promise<inferRouterContext<TRouter>>;
export type FetchCreateContextOption<TRouter extends AnyRouter> =
CreateContextCallback<
inferRouterContext<TRouter>,
FetchCreateContextFn<TRouter>
>;
export type FetchHandlerOptions<TRouter extends AnyRouter> =
FetchCreateContextOption<TRouter> &
HTTPBaseHandlerOptions<TRouter, Request> & {
req: Request;
endpoint: string;
};
export type FetchHandlerRequestOptions<TRouter extends AnyRouter> =
HTTPBaseHandlerOptions<TRouter, Request> &
CreateContextCallback<
inferRouterContext<TRouter>,
FetchCreateContextFn<TRouter>
> & {
req: Request;
endpoint: string;
};

View File

@@ -0,0 +1,7 @@
export { nextAppDirCaller as experimental_nextAppDirCaller } from './next-app-dir/nextAppDirCaller';
export { redirect as experimental_redirect } from './next-app-dir/redirect';
export { notFound as experimental_notFound } from './next-app-dir/notFound';
export {
/** @internal */
rethrowNextErrors,
} from './next-app-dir/rethrowNextErrors';

View File

@@ -0,0 +1,135 @@
import type { CreateContextCallback } from '../../@trpc/server';
import { getTRPCErrorFromUnknown, TRPCError } from '../../@trpc/server';
// eslint-disable-next-line no-restricted-imports
import { formDataToObject } from '../../unstable-core-do-not-import';
// FIXME: fix lint rule, this is ok
// eslint-disable-next-line no-restricted-imports
import type { ErrorHandlerOptions } from '../../unstable-core-do-not-import/procedure';
// FIXME: fix lint rule, this is ok
// eslint-disable-next-line no-restricted-imports
import type { CallerOverride } from '../../unstable-core-do-not-import/procedureBuilder';
// FIXME: fix lint rule, this is ok
// eslint-disable-next-line no-restricted-imports
import type {
MaybePromise,
Simplify,
} from '../../unstable-core-do-not-import/types';
import { TRPCRedirectError } from './redirect';
import { rethrowNextErrors } from './rethrowNextErrors';
/**
* Create a caller that works with Next.js React Server Components & Server Actions
*/
export function nextAppDirCaller<TContext, TMeta>(
config: Simplify<
{
/**
* Extract the path from the procedure metadata
*/
pathExtractor?: (opts: { meta: TMeta }) => string;
/**
* Transform form data to a `Record` before passing it to the procedure
* @default true
*/
normalizeFormData?: boolean;
/**
* Called when an error occurs in the handler
*/
onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} & CreateContextCallback<TContext, () => MaybePromise<TContext>>
>,
): CallerOverride<TContext> {
const {
normalizeFormData = true,
// rethrowNextErrors = true
} = config;
const createContext = async (): Promise<TContext> => {
return config?.createContext?.() ?? ({} as TContext);
};
return async (opts) => {
const path =
config.pathExtractor?.({ meta: opts._def.meta as TMeta }) ?? '';
const ctx: TContext = await createContext().catch((cause) => {
const error = new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to create context',
cause,
});
throw error;
});
const handleError = (cause: unknown) => {
const error = getTRPCErrorFromUnknown(cause);
config.onError?.({
ctx,
error,
input: opts.args[0],
path,
type: opts._def.type,
});
rethrowNextErrors(error);
throw error;
};
switch (opts._def.type) {
case 'mutation': {
/**
* When you wrap an action with useFormState, it gets an extra argument as its first argument.
* The submitted form data is therefore its second argument instead of its first as it would usually be.
* The new first argument that gets added is the current state of the form.
* @see https://react.dev/reference/react-dom/hooks/useFormState#my-action-can-no-longer-read-the-submitted-form-data
*/
let input = opts.args.length === 1 ? opts.args[0] : opts.args[1];
if (normalizeFormData && input instanceof FormData) {
input = formDataToObject(input);
}
return await opts
.invoke({
type: opts._def.type,
ctx,
getRawInput: async () => input,
path,
input,
signal: undefined,
batchIndex: 0,
})
.then((data) => {
if (data instanceof TRPCRedirectError) throw data;
return data;
})
.catch(handleError);
}
case 'query': {
const input = opts.args[0];
return await opts
.invoke({
type: opts._def.type,
ctx,
getRawInput: async () => input,
path,
input,
signal: undefined,
batchIndex: 0,
})
.then((data) => {
if (data instanceof TRPCRedirectError) throw data;
return data;
})
.catch(handleError);
}
case 'subscription':
default: {
throw new TRPCError({
code: 'NOT_IMPLEMENTED',
message: `Not implemented for type ${opts._def.type}`,
});
}
}
};
}

View File

@@ -0,0 +1,12 @@
import type { notFound as __notFound } from 'next/navigation';
import { TRPCError } from '../../@trpc/server';
/**
* Like `next/navigation`'s `notFound()` but throws a `TRPCError` that later will be handled by Next.js
* @public
*/
export const notFound: typeof __notFound = () => {
throw new TRPCError({
code: 'NOT_FOUND',
});
};

View File

@@ -0,0 +1,30 @@
import type { RedirectType } from 'next/navigation';
import { TRPCError } from '../../@trpc/server';
/**
* @internal
*/
export class TRPCRedirectError extends TRPCError {
public readonly args;
constructor(url: URL | string, redirectType?: RedirectType) {
super({
// TODO(?): This should maybe a custom error code
code: 'UNPROCESSABLE_CONTENT',
message: `Redirect error to "${url}" that will be handled by Next.js`,
});
this.args = [url.toString(), redirectType] as const;
}
}
/**
* Like `next/navigation`'s `redirect()` but throws a `TRPCError` that later will be handled by Next.js
* This provides better typesafety than the `next/navigation`'s `redirect()` since the action continues
* to execute on the frontend even if Next's `redirect()` has a return type of `never`.
* @public
* @remark You should only use this if you're also using `nextAppDirCaller`.
*/
export const redirect = (url: URL | string, redirectType?: RedirectType) => {
// We rethrow this internally so the returntype on the client is undefined.
return new TRPCRedirectError(url, redirectType) as unknown as undefined;
};

View File

@@ -0,0 +1,68 @@
import * as nextNavigation from 'next/navigation';
import type { TRPCError } from '../../@trpc/server';
import { TRPCRedirectError } from './redirect';
/**
* @remarks The helpers from `next/dist/client/components/*` has been removed in Next.js 15.
* Inlining them here instead...
* @see https://github.com/vercel/next.js/blob/5ae286ffd664e5c76841ed64f6e2da85a0835922/packages/next/src/client/components/redirect.ts#L97-L123
*/
const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT';
function isRedirectError(error: unknown) {
if (
typeof error !== 'object' ||
error === null ||
!('digest' in error) ||
typeof error.digest !== 'string'
) {
return false;
}
const [errorCode, type, destination, status] = error.digest.split(';', 4);
const statusCode = Number(status);
return (
errorCode === REDIRECT_ERROR_CODE &&
(type === 'replace' || type === 'push') &&
typeof destination === 'string' &&
!isNaN(statusCode)
);
}
/**
* @remarks The helpers from `next/dist/client/components/*` has been removed in Next.js 15.
* Inlining them here instead...
* @see https://github.com/vercel/next.js/blob/5ae286ffd664e5c76841ed64f6e2da85a0835922/packages/next/src/client/components/not-found.ts#L33-L39
*/
const NOT_FOUND_ERROR_CODE = 'NEXT_NOT_FOUND';
function isNotFoundError(error: unknown) {
if (typeof error !== 'object' || error === null || !('digest' in error)) {
return false;
}
return error.digest === NOT_FOUND_ERROR_CODE;
}
/**
* Rethrow errors that should be handled by Next.js
*/
export const rethrowNextErrors = (error: TRPCError) => {
if (error.code === 'NOT_FOUND') {
nextNavigation.notFound();
}
if (error instanceof TRPCRedirectError) {
nextNavigation.redirect(...error.args);
}
const { cause } = error;
// Next.js 15 has `unstable_rethrow`. Use that if it exists.
if (
'unstable_rethrow' in nextNavigation &&
typeof nextNavigation.unstable_rethrow === 'function'
) {
nextNavigation.unstable_rethrow(cause);
}
// Before Next.js 15, we have to check and rethrow the error manually.
if (isRedirectError(cause) || isNotFoundError(cause)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
throw cause!;
}
};

69
node_modules/@trpc/server/src/adapters/next.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
// @trpc/server
import type { AnyRouter } from '../@trpc/server';
// @trpc/server
import { TRPCError } from '../@trpc/server';
// eslint-disable-next-line no-restricted-imports
import { run } from '../unstable-core-do-not-import';
import type {
NodeHTTPCreateContextFnOptions,
NodeHTTPHandlerOptions,
} from './node-http';
import { internal_exceptionHandler, nodeHTTPRequestHandler } from './node-http';
export type CreateNextContextOptions = NodeHTTPCreateContextFnOptions<
NextApiRequest,
NextApiResponse
>;
/**
* Preventing "TypeScript where it's tough not to get "The inferred type of 'xxxx' cannot be named without a reference to [...]"
*/
export type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
export function createNextApiHandler<TRouter extends AnyRouter>(
opts: NodeHTTPHandlerOptions<TRouter, NextApiRequest, NextApiResponse>,
): NextApiHandler {
return async (req, res) => {
let path = '';
await run(async () => {
path = run(() => {
if (typeof req.query['trpc'] === 'string') {
return req.query['trpc'];
}
if (Array.isArray(req.query['trpc'])) {
return req.query['trpc'].join('/');
}
throw new TRPCError({
message:
'Query "trpc" not found - is the file named `[trpc]`.ts or `[...trpc].ts`?',
code: 'INTERNAL_SERVER_ERROR',
});
});
await nodeHTTPRequestHandler({
...(opts as any),
req,
res,
path,
});
}).catch(
internal_exceptionHandler({
req,
res,
path,
...opts,
}),
);
};
}

View File

@@ -0,0 +1,167 @@
import type * as http from 'http';
import { IncomingMessage } from 'node:http';
import { TRPCError } from '../../@trpc/server';
import type { NodeHTTPRequest, NodeHTTPResponse } from './types';
function createBody(
req: NodeHTTPRequest,
opts: {
/**
* Max body size in bytes. If the body is larger than this, the request will be aborted
*/
maxBodySize: number | null;
},
): RequestInit['body'] {
// Some adapters will pre-parse the body and add it to the request object
if ('body' in req) {
if (req.body === undefined) {
// If body property exists but is undefined, return undefined
return undefined;
}
// If the body is already a string, return it directly
if (typeof req.body === 'string') {
return req.body;
}
// formData use
if (req.body instanceof IncomingMessage) {
return req.body as any;
}
// If body exists but isn't a string, stringify it as JSON
return JSON.stringify(req.body);
}
let size = 0;
let hasClosed = false;
return new ReadableStream({
start(controller) {
const onData = (chunk: Buffer) => {
size += chunk.length;
if (!opts.maxBodySize || size <= opts.maxBodySize) {
controller.enqueue(
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
);
return;
}
controller.error(
new TRPCError({
code: 'PAYLOAD_TOO_LARGE',
}),
);
hasClosed = true;
req.off('data', onData);
req.off('end', onEnd);
};
const onEnd = () => {
if (hasClosed) {
return;
}
hasClosed = true;
req.off('data', onData);
req.off('end', onEnd);
controller.close();
};
req.on('data', onData);
req.on('end', onEnd);
},
cancel() {
req.destroy();
},
});
}
export function createURL(req: NodeHTTPRequest): URL {
try {
const protocol =
// http2
(req.headers[':scheme'] && req.headers[':scheme'] === 'https') ||
// http1
(req.socket && 'encrypted' in req.socket && req.socket.encrypted)
? 'https:'
: 'http:';
const host = req.headers.host ?? req.headers[':authority'] ?? 'localhost';
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return new URL(req.url!, `${protocol}//${host}`);
} catch (cause) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Invalid URL',
cause,
});
}
}
function createHeaders(incoming: http.IncomingHttpHeaders): Headers {
const headers = new Headers();
for (const key in incoming) {
const value = incoming[key];
if (typeof key === 'string' && key.startsWith(':')) {
// Skip HTTP/2 pseudo-headers
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
headers.append(key, item);
}
} else if (value != null) {
headers.append(key, value);
}
}
return headers;
}
/**
* Convert an [`IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage) to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
*/
export function incomingMessageToRequest(
req: NodeHTTPRequest,
res: NodeHTTPResponse,
opts: {
/**
* Max body size in bytes. If the body is larger than this, the request will be aborted
*/
maxBodySize: number | null;
},
): Request {
const ac = new AbortController();
const onAbort = () => {
res.off('close', onAbort);
req.off('aborted', onAbort);
// abort the request
ac.abort();
};
res.once('close', onAbort);
req.once('aborted', onAbort);
// Get host from either regular header or HTTP/2 pseudo-header
const url = createURL(req);
const init: RequestInit = {
headers: createHeaders(req.headers),
method: req.method,
signal: ac.signal,
};
if (req.method !== 'GET' && req.method !== 'HEAD') {
init.body = createBody(req, opts);
// init.duplex = 'half' must be set when body is a ReadableStream, and Node follows the spec.
// However, this property is not defined in the TypeScript types for RequestInit, so we have
// to cast it here in order to set it without a type error.
// See https://fetch.spec.whatwg.org/#dom-requestinit-duplex
// @ts-expect-error this is fine
init.duplex = 'half';
}
const request = new Request(url, init);
return request;
}

View File

@@ -0,0 +1,3 @@
export * from './nodeHTTPRequestHandler';
export * from './types';
export * from './incomingMessageToRequest';

View File

@@ -0,0 +1,121 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
// @trpc/server
import {
getTRPCErrorFromUnknown,
transformTRPCResponse,
type AnyRouter,
} from '../../@trpc/server';
import type { ResolveHTTPRequestOptionsContextFn } from '../../@trpc/server/http';
import { resolveResponse } from '../../@trpc/server/http';
// eslint-disable-next-line no-restricted-imports
import { getErrorShape, run } from '../../unstable-core-do-not-import';
import { incomingMessageToRequest } from './incomingMessageToRequest';
import type {
NodeHTTPRequest,
NodeHTTPRequestHandlerOptions,
NodeHTTPResponse,
} from './types';
import { writeResponse } from './writeResponse';
/**
* @internal
*/
export function internal_exceptionHandler<
TRouter extends AnyRouter,
TRequest extends NodeHTTPRequest,
TResponse extends NodeHTTPResponse,
>(opts: NodeHTTPRequestHandlerOptions<TRouter, TRequest, TResponse>) {
return (cause: unknown) => {
const { res, req } = opts;
const error = getTRPCErrorFromUnknown(cause);
const shape = getErrorShape({
config: opts.router._def._config,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx: undefined,
});
opts.onError?.({
req,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx: undefined,
});
const transformed = transformTRPCResponse(opts.router._def._config, {
error: shape,
});
res.statusCode = shape.data.httpStatus;
res.end(JSON.stringify(transformed));
};
}
/**
* @remark the promise never rejects
*/
export async function nodeHTTPRequestHandler<
TRouter extends AnyRouter,
TRequest extends NodeHTTPRequest,
TResponse extends NodeHTTPResponse,
>(opts: NodeHTTPRequestHandlerOptions<TRouter, TRequest, TResponse>) {
return new Promise<void>((resolve) => {
const handleViaMiddleware =
opts.middleware ?? ((_req, _res, next) => next());
opts.res.once('finish', () => {
resolve();
});
return handleViaMiddleware(opts.req, opts.res, (err: unknown) => {
run(async () => {
const request = incomingMessageToRequest(opts.req, opts.res, {
maxBodySize: opts.maxBodySize ?? null,
});
// Build tRPC dependencies
const createContext: ResolveHTTPRequestOptionsContextFn<
TRouter
> = async (innerOpts) => {
return await opts.createContext?.({
...opts,
...innerOpts,
});
};
const response = await resolveResponse({
...opts,
req: request,
error: err ? getTRPCErrorFromUnknown(err) : null,
createContext,
onError(o) {
opts?.onError?.({
...o,
req: opts.req,
});
},
});
await writeResponse({
request,
response,
rawResponse: opts.res,
});
}).catch(internal_exceptionHandler(opts));
});
});
}

View File

@@ -0,0 +1,132 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
import type * as http from 'http';
import type * as http2 from 'http2';
// @trpc/server
import type {
AnyRouter,
CreateContextCallback,
inferRouterContext,
} from '../../@trpc/server';
// @trpc/server/http
import type {
HTTPBaseHandlerOptions,
TRPCRequestInfo,
} from '../../@trpc/server/http';
// eslint-disable-next-line no-restricted-imports
import type {
DistributiveOmit,
MaybePromise,
} from '../../unstable-core-do-not-import';
export type NodeHTTPRequest = DistributiveOmit<
http.IncomingMessage | http2.Http2ServerRequest,
'socket'
> & {
/**
* Many adapters will add a `body` property to the incoming message and pre-parse the body
*/
body?: unknown;
/**
* Socket is not always available in all deployments, so we need to make it optional
* @see https://github.com/trpc/trpc/issues/6341
* The socket object provided in the request does not fully implement the expected Node.js Socket interface.
* @see https://github.com/trpc/trpc/pull/6358
*/
socket?:
| Partial<http.IncomingMessage['socket']>
| Partial<http2.Http2ServerRequest['socket']>;
};
export type NodeHTTPResponse = DistributiveOmit<
http.ServerResponse | http2.Http2ServerResponse,
'write'
> & {
/**
* Force the partially-compressed response to be flushed to the client.
*
* Added by compression middleware
* (depending on the environment,
* e.g. Next 12 and below,
* e.g. Express w/ `compression()`)
*/
flush?: () => void;
write: (chunk: string | Uint8Array) => boolean;
};
export type NodeHTTPCreateContextOption<
TRouter extends AnyRouter,
TRequest,
TResponse,
> = CreateContextCallback<
inferRouterContext<TRouter>,
NodeHTTPCreateContextFn<TRouter, TRequest, TResponse>
>;
/**
* @internal
*/
type ConnectMiddleware<
TRequest extends NodeHTTPRequest = NodeHTTPRequest,
TResponse extends NodeHTTPResponse = NodeHTTPResponse,
> = (req: TRequest, res: TResponse, next: (err?: any) => any) => void;
export type NodeHTTPHandlerOptions<
TRouter extends AnyRouter,
TRequest extends NodeHTTPRequest,
TResponse extends NodeHTTPResponse,
> = HTTPBaseHandlerOptions<TRouter, TRequest> &
NodeHTTPCreateContextOption<TRouter, TRequest, TResponse> & {
/**
* By default, http `OPTIONS` requests are not handled, and CORS headers are not returned.
*
* This can be used to handle them manually or via the `cors` npm package: https://www.npmjs.com/package/cors
*
* ```ts
* import cors from 'cors'
*
* nodeHTTPRequestHandler({
* middleware: cors()
* })
* ```
*
* You can also use it for other needs which a connect/node.js compatible middleware can solve,
* though you might wish to consider an alternative solution like the Express adapter if your needs are complex.
*/
middleware?: ConnectMiddleware<TRequest, TResponse>;
maxBodySize?: number;
};
export type NodeHTTPRequestHandlerOptions<
TRouter extends AnyRouter,
TRequest extends NodeHTTPRequest,
TResponse extends NodeHTTPResponse,
> = NodeHTTPHandlerOptions<TRouter, TRequest, TResponse> & {
req: TRequest;
res: TResponse;
/**
* The tRPC path to handle requests for
* @example 'post.all'
*/
path: string;
};
export type NodeHTTPCreateContextFnOptions<TRequest, TResponse> = {
req: TRequest;
res: TResponse;
info: TRPCRequestInfo;
};
export type NodeHTTPCreateContextFn<
TRouter extends AnyRouter,
TRequest,
TResponse,
> = (
opts: NodeHTTPCreateContextFnOptions<TRequest, TResponse>,
) => MaybePromise<inferRouterContext<TRouter>>;

View File

@@ -0,0 +1,99 @@
// eslint-disable-next-line no-restricted-imports
import { isAbortError } from '../../unstable-core-do-not-import';
import type { NodeHTTPResponse } from './types';
async function writeResponseBodyChunk(
res: NodeHTTPResponse,
chunk: Uint8Array,
) {
// useful for debugging 🙃
// console.debug('writing', new TextDecoder().decode(chunk));
if (res.write(chunk) === false) {
await new Promise<void>((resolve, reject) => {
const onError = (err: unknown) => {
reject(err);
cleanup();
};
const onDrain = () => {
resolve();
cleanup();
};
const cleanup = () => {
res.off('error', onError);
res.off('drain', onDrain);
};
res.once('error', onError);
res.once('drain', onDrain);
});
}
}
/**
* @internal
*/
export async function writeResponseBody(opts: {
res: NodeHTTPResponse;
signal: AbortSignal;
body: NonNullable<Response['body']>;
}) {
const { res } = opts;
try {
const writableStream = new WritableStream({
async write(chunk) {
await writeResponseBodyChunk(res, chunk);
res.flush?.();
},
});
await opts.body.pipeTo(writableStream, {
signal: opts.signal,
});
} catch (err) {
if (isAbortError(err)) {
return;
}
throw err;
}
}
/**
* @internal
*/
export async function writeResponse(opts: {
request: Request;
response: Response;
rawResponse: NodeHTTPResponse;
}) {
const { response, rawResponse } = opts;
// Only override status code if it hasn't been explicitly set in a procedure etc
if (rawResponse.statusCode === 200) {
rawResponse.statusCode = response.status;
}
for (const [key, value] of response.headers) {
if (key.toLowerCase() === 'set-cookie') continue;
rawResponse.setHeader(key, value);
}
const cookies = response.headers.getSetCookie();
if (cookies.length > 0) {
rawResponse.setHeader('set-cookie', cookies);
}
try {
if (response.body) {
await writeResponseBody({
res: rawResponse,
signal: opts.request.signal,
body: response.body,
});
}
} catch (err) {
if (!rawResponse.headersSent) {
rawResponse.statusCode = 500;
}
throw err;
} finally {
rawResponse.end();
}
}

121
node_modules/@trpc/server/src/adapters/standalone.ts generated vendored Normal file
View File

@@ -0,0 +1,121 @@
/**
* If you're making an adapter for tRPC and looking at this file for reference, you should import types and functions from `@trpc/server` and `@trpc/server/http`
*
* @example
* ```ts
* import type { AnyTRPCRouter } from '@trpc/server'
* import type { HTTPBaseHandlerOptions } from '@trpc/server/http'
* ```
*/
import http from 'http';
// --- http2 ---
import type * as http2 from 'http2';
// @trpc/server
import { type AnyRouter } from '../@trpc/server';
// eslint-disable-next-line no-restricted-imports
import { run } from '../unstable-core-do-not-import';
import type {
NodeHTTPCreateContextFnOptions,
NodeHTTPHandlerOptions,
NodeHTTPRequest,
NodeHTTPResponse,
} from './node-http';
import {
createURL,
internal_exceptionHandler,
nodeHTTPRequestHandler,
} from './node-http';
type StandaloneHandlerOptions<
TRouter extends AnyRouter,
TRequest extends NodeHTTPRequest,
TResponse extends NodeHTTPResponse,
> = NodeHTTPHandlerOptions<TRouter, TRequest, TResponse> & {
/**
* The base path to handle requests for.
* This will be sliced from the beginning of the request path
* (Do not miss including the trailing slash)
* @default '/'
* @example '/trpc/'
* @example '/trpc/api/'
*/
basePath?: string;
};
// --- http1 ---
export type CreateHTTPHandlerOptions<TRouter extends AnyRouter> =
StandaloneHandlerOptions<TRouter, http.IncomingMessage, http.ServerResponse>;
export type CreateHTTPContextOptions = NodeHTTPCreateContextFnOptions<
http.IncomingMessage,
http.ServerResponse
>;
function createHandler<
TRouter extends AnyRouter,
TRequest extends NodeHTTPRequest,
TResponse extends NodeHTTPResponse,
>(
opts: StandaloneHandlerOptions<TRouter, TRequest, TResponse>,
): (req: TRequest, res: TResponse) => void {
const basePath = opts.basePath ?? '/';
const sliceLength = basePath.length;
return (req, res) => {
let path = '';
run(async () => {
const url = createURL(req);
// get procedure(s) path and remove the leading slash
path = url.pathname.slice(sliceLength);
await nodeHTTPRequestHandler({
...(opts as any),
req,
res,
path,
});
}).catch(
internal_exceptionHandler({
req,
res,
path,
...opts,
}),
);
};
}
/**
* @internal
*/
export function createHTTPHandler<TRouter extends AnyRouter>(
opts: CreateHTTPHandlerOptions<TRouter>,
): http.RequestListener {
return createHandler(opts);
}
export function createHTTPServer<TRouter extends AnyRouter>(
opts: CreateHTTPHandlerOptions<TRouter>,
) {
return http.createServer(createHTTPHandler(opts));
}
// --- http2 ---
export type CreateHTTP2HandlerOptions<TRouter extends AnyRouter> =
StandaloneHandlerOptions<
TRouter,
http2.Http2ServerRequest,
http2.Http2ServerResponse
>;
export type CreateHTTP2ContextOptions = NodeHTTPCreateContextFnOptions<
http2.Http2ServerRequest,
http2.Http2ServerResponse
>;
export function createHTTP2Handler(opts: CreateHTTP2HandlerOptions<AnyRouter>) {
return createHandler(opts);
}

644
node_modules/@trpc/server/src/adapters/ws.ts generated vendored Normal file
View File

@@ -0,0 +1,644 @@
import type { IncomingMessage } from 'http';
import type ws from 'ws';
import type {
AnyRouter,
CreateContextCallback,
inferRouterContext,
} from '../@trpc/server';
import {
callTRPCProcedure,
getErrorShape,
getTRPCErrorFromUnknown,
transformTRPCResponse,
TRPCError,
} from '../@trpc/server';
import type { TRPCRequestInfo } from '../@trpc/server/http';
import { type BaseHandlerOptions } from '../@trpc/server/http';
import { parseTRPCMessage } from '../@trpc/server/rpc';
// @trpc/server/rpc
import type {
TRPCClientOutgoingMessage,
TRPCConnectionParamsMessage,
TRPCReconnectNotification,
TRPCResponseMessage,
TRPCResultMessage,
} from '../@trpc/server/rpc';
import { parseConnectionParamsFromUnknown } from '../http';
import { isObservable, observableToAsyncIterable } from '../observable';
// eslint-disable-next-line no-restricted-imports
import {
isAsyncIterable,
isObject,
isTrackedEnvelope,
run,
type MaybePromise,
} from '../unstable-core-do-not-import';
// eslint-disable-next-line no-restricted-imports
import type { Result } from '../unstable-core-do-not-import';
// eslint-disable-next-line no-restricted-imports
import { iteratorResource } from '../unstable-core-do-not-import/stream/utils/asyncIterable';
import { Unpromise } from '../vendor/unpromise';
import { createURL, type NodeHTTPCreateContextFnOptions } from './node-http';
import type { Encoder } from './wsEncoder';
import { jsonEncoder } from './wsEncoder';
/**
* Importing ws causes a build error
* @see https://github.com/trpc/trpc/pull/5279
*/
const WEBSOCKET_OPEN = 1; /* ws.WebSocket.OPEN */
/**
* @public
*/
export type CreateWSSContextFnOptions = NodeHTTPCreateContextFnOptions<
IncomingMessage,
ws.WebSocket
>;
/**
* @public
*/
export type CreateWSSContextFn<TRouter extends AnyRouter> = (
opts: CreateWSSContextFnOptions,
) => MaybePromise<inferRouterContext<TRouter>>;
export type WSConnectionHandlerOptions<TRouter extends AnyRouter> =
BaseHandlerOptions<TRouter, IncomingMessage> &
CreateContextCallback<
inferRouterContext<TRouter>,
CreateWSSContextFn<TRouter>
>;
/**
* Web socket server handler
*/
export type WSSHandlerOptions<TRouter extends AnyRouter> =
WSConnectionHandlerOptions<TRouter> & {
wss: ws.WebSocketServer;
prefix?: string;
keepAlive?: {
/**
* Enable heartbeat messages
* @default false
*/
enabled: boolean;
/**
* Heartbeat interval in milliseconds
* @default 30_000
*/
pingMs?: number;
/**
* Terminate the WebSocket if no pong is received after this many milliseconds
* @default 5_000
*/
pongWaitMs?: number;
};
/**
* Disable responding to ping messages from the client
* **Not recommended** - this is mainly used for testing
* @default false
*/
dangerouslyDisablePong?: boolean;
/**
* Custom encoder for wire encoding (e.g. custom binary formats)
* @default jsonEncoder
*/
experimental_encoder?: Encoder;
};
export function getWSConnectionHandler<TRouter extends AnyRouter>(
opts: WSSHandlerOptions<TRouter>,
) {
const { createContext, router } = opts;
const { transformer } = router._def._config;
const encoder = opts.experimental_encoder ?? jsonEncoder;
return (client: ws.WebSocket, req: IncomingMessage) => {
type Context = inferRouterContext<TRouter>;
type ContextResult = Result<Context>;
const clientSubscriptions = new Map<number | string, AbortController>();
const abortController = new AbortController();
if (opts.keepAlive?.enabled) {
const { pingMs, pongWaitMs } = opts.keepAlive;
handleKeepAlive(client, pingMs, pongWaitMs);
}
function respond(untransformedJSON: TRPCResponseMessage) {
client.send(
encoder.encode(
transformTRPCResponse(router._def._config, untransformedJSON),
),
);
}
async function createCtxPromise(
getConnectionParams: () => TRPCRequestInfo['connectionParams'],
): Promise<ContextResult> {
try {
return await run(async (): Promise<ContextResult> => {
ctx = await createContext?.({
req,
res: client,
info: {
connectionParams: getConnectionParams(),
calls: [],
isBatchCall: false,
accept: null,
type: 'unknown',
signal: abortController.signal,
url: null,
},
});
return {
ok: true,
value: ctx,
};
});
} catch (cause) {
const error = getTRPCErrorFromUnknown(cause);
opts.onError?.({
error,
path: undefined,
type: 'unknown',
ctx,
req,
input: undefined,
});
respond({
id: null,
error: getErrorShape({
config: router._def._config,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx,
}),
});
// close in next tick
(globalThis.setImmediate ?? globalThis.setTimeout)(() => {
client.close();
});
return {
ok: false,
error,
};
}
}
let ctx: Context | undefined = undefined;
/**
* promise for initializing the context
*
* - the context promise will be created immediately on connection if no connectionParams are expected
* - if connection params are expected, they will be created once received
*/
let ctxPromise =
createURL(req).searchParams.get('connectionParams') === '1'
? null
: createCtxPromise(() => null);
function handleRequest(msg: TRPCClientOutgoingMessage, batchIndex: number) {
const { id, jsonrpc } = msg;
if (id === null) {
const error = getTRPCErrorFromUnknown(
new TRPCError({
code: 'PARSE_ERROR',
message: '`id` is required',
}),
);
opts.onError?.({
error,
path: undefined,
type: 'unknown',
ctx,
req,
input: undefined,
});
respond({
id,
jsonrpc,
error: getErrorShape({
config: router._def._config,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx,
}),
});
return;
}
if (msg.method === 'subscription.stop') {
clientSubscriptions.get(id)?.abort();
return;
}
const { path, lastEventId } = msg.params;
let { input } = msg.params;
const type = msg.method;
if (lastEventId !== undefined) {
if (isObject(input)) {
input = {
...input,
lastEventId: lastEventId,
};
} else {
input ??= {
lastEventId: lastEventId,
};
}
}
run(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const res = await ctxPromise!; // asserts context has been set
if (!res.ok) {
throw res.error;
}
const abortController = new AbortController();
const result = await callTRPCProcedure({
router,
path,
getRawInput: async () => input,
ctx,
type,
signal: abortController.signal,
batchIndex,
});
const isIterableResult =
isAsyncIterable(result) || isObservable(result);
if (type !== 'subscription') {
if (isIterableResult) {
throw new TRPCError({
code: 'UNSUPPORTED_MEDIA_TYPE',
message: `Cannot return an async iterable or observable from a ${type} procedure with WebSockets`,
});
}
// send the value as data if the method is not a subscription
respond({
id,
jsonrpc,
result: {
type: 'data',
data: result,
},
});
return;
}
if (!isIterableResult) {
throw new TRPCError({
message: `Subscription ${path} did not return an observable or a AsyncGenerator`,
code: 'INTERNAL_SERVER_ERROR',
});
}
/* istanbul ignore next -- @preserve */
if (client.readyState !== WEBSOCKET_OPEN) {
// if the client got disconnected whilst initializing the subscription
// no need to send stopped message if the client is disconnected
return;
}
/* istanbul ignore next -- @preserve */
if (clientSubscriptions.has(id)) {
// duplicate request ids for client
throw new TRPCError({
message: `Duplicate id ${id}`,
code: 'BAD_REQUEST',
});
}
const iterable = isObservable(result)
? observableToAsyncIterable(result, abortController.signal)
: result;
run(async () => {
await using iterator = iteratorResource(iterable);
const abortPromise = new Promise<'abort'>((resolve) => {
abortController.signal.onabort = () => resolve('abort');
});
// We need those declarations outside the loop for garbage collection reasons. If they
// were declared inside, they would not be freed until the next value is present.
let next:
| null
| TRPCError
| Awaited<
typeof abortPromise | ReturnType<(typeof iterator)['next']>
>;
let result: null | TRPCResultMessage<unknown>['result'];
while (true) {
next = await Unpromise.race([
iterator.next().catch(getTRPCErrorFromUnknown),
abortPromise,
]);
if (next === 'abort') {
await iterator.return?.();
break;
}
if (next instanceof Error) {
const error = getTRPCErrorFromUnknown(next);
opts.onError?.({ error, path, type, ctx, req, input });
respond({
id,
jsonrpc,
error: getErrorShape({
config: router._def._config,
error,
type,
path,
input,
ctx,
}),
});
break;
}
if (next.done) {
break;
}
result = {
type: 'data',
data: next.value,
};
if (isTrackedEnvelope(next.value)) {
const [id, data] = next.value;
result.id = id;
result.data = {
id,
data,
};
}
respond({
id,
jsonrpc,
result,
});
// free up references for garbage collection
next = null;
result = null;
}
respond({
id,
jsonrpc,
result: {
type: 'stopped',
},
});
clientSubscriptions.delete(id);
}).catch((cause) => {
const error = getTRPCErrorFromUnknown(cause);
opts.onError?.({ error, path, type, ctx, req, input });
respond({
id,
jsonrpc,
error: getErrorShape({
config: router._def._config,
error,
type,
path,
input,
ctx,
}),
});
abortController.abort();
});
clientSubscriptions.set(id, abortController);
respond({
id,
jsonrpc,
result: {
type: 'started',
},
});
}).catch((cause) => {
// procedure threw an error
const error = getTRPCErrorFromUnknown(cause);
opts.onError?.({ error, path, type, ctx, req, input });
respond({
id,
jsonrpc,
error: getErrorShape({
config: router._def._config,
error,
type,
path,
input,
ctx,
}),
});
});
}
client.on('message', (rawData, isBinary) => {
// Handle PING/PONG as text regardless of encoder
if (!isBinary) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const msgStr = rawData.toString();
if (msgStr === 'PONG') {
return;
}
if (msgStr === 'PING') {
if (!opts.dangerouslyDisablePong) {
client.send('PONG');
}
return;
}
}
// Convert rawData to a format our encoder accepts
// ws gives us Buffer for both text and binary frames
if (!Buffer.isBuffer(rawData)) {
const error = new TRPCError({
code: 'UNPROCESSABLE_CONTENT',
message: 'Unexpected WebSocket message format',
});
respond({
id: null,
error: getErrorShape({
config: router._def._config,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx,
}),
});
return;
}
const data: string | Uint8Array = isBinary
? rawData
: rawData.toString('utf8');
if (!ctxPromise) {
// If the ctxPromise wasn't created immediately, we're expecting the first message to be a TRPCConnectionParamsMessage
ctxPromise = createCtxPromise(() => {
let msg;
try {
msg = encoder.decode(data) as TRPCConnectionParamsMessage;
if (!isObject(msg)) {
throw new Error('Message was not an object');
}
} catch (cause) {
throw new TRPCError({
code: 'PARSE_ERROR',
message: `Malformed TRPCConnectionParamsMessage`,
cause,
});
}
const connectionParams = parseConnectionParamsFromUnknown(msg.data);
return connectionParams;
});
return;
}
const parsedMsgs = run(() => {
try {
const msgJSON: unknown = encoder.decode(data);
const msgs: unknown[] = Array.isArray(msgJSON) ? msgJSON : [msgJSON];
return msgs.map((raw) => parseTRPCMessage(raw, transformer));
} catch (cause) {
const error = new TRPCError({
code: 'PARSE_ERROR',
cause,
});
respond({
id: null,
error: getErrorShape({
config: router._def._config,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx,
}),
});
return [];
}
});
parsedMsgs.map((msg, index) => handleRequest(msg, index));
});
// WebSocket errors should be handled, as otherwise unhandled exceptions will crash Node.js.
// This line was introduced after the following error brought down production systems:
// "RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear"
// Here is the relevant discussion: https://github.com/websockets/ws/issues/1354#issuecomment-774616962
client.on('error', (cause) => {
opts.onError?.({
ctx,
error: getTRPCErrorFromUnknown(cause),
input: undefined,
path: undefined,
type: 'unknown',
req,
});
});
client.once('close', () => {
for (const sub of clientSubscriptions.values()) {
sub.abort();
}
clientSubscriptions.clear();
abortController.abort();
});
};
}
/**
* Handle WebSocket keep-alive messages
*/
export function handleKeepAlive(
client: ws.WebSocket,
pingMs = 30_000,
pongWaitMs = 5_000,
) {
let timeout: NodeJS.Timeout | undefined = undefined;
let ping: NodeJS.Timeout | undefined = undefined;
const schedulePing = () => {
const scheduleTimeout = () => {
timeout = setTimeout(() => {
client.terminate();
}, pongWaitMs) as any;
};
ping = setTimeout(() => {
client.send('PING');
scheduleTimeout();
}, pingMs) as any;
};
const onMessage = () => {
clearTimeout(ping);
clearTimeout(timeout);
schedulePing();
};
client.on('message', onMessage);
client.on('close', () => {
clearTimeout(ping);
clearTimeout(timeout);
});
schedulePing();
}
export function applyWSSHandler<TRouter extends AnyRouter>(
opts: WSSHandlerOptions<TRouter>,
) {
const encoder = opts.experimental_encoder ?? jsonEncoder;
const onConnection = getWSConnectionHandler(opts);
opts.wss.on('connection', (client, req) => {
if (opts.prefix && !req.url?.startsWith(opts.prefix)) {
return;
}
onConnection(client, req);
});
return {
broadcastReconnectNotification: () => {
const response: TRPCReconnectNotification = {
id: null,
method: 'reconnect',
};
const data = encoder.encode(response);
for (const client of opts.wss.clients) {
if (client.readyState === WEBSOCKET_OPEN) {
client.send(data);
}
}
},
};
}
export type { Encoder } from './wsEncoder';
export { jsonEncoder } from './wsEncoder';

35
node_modules/@trpc/server/src/adapters/wsEncoder.ts generated vendored Normal file
View File

@@ -0,0 +1,35 @@
/**
* Encoder for WebSocket wire format.
* Encodes outgoing messages and decodes incoming messages.
*
* @example
* ```ts
* const customEncoder: Encoder = {
* encode: (data) => myFormat.stringify(data),
* decode: (data) => myFormat.parse(data),
* };
* ```
*/
export interface Encoder {
/** Encode data for transmission over the wire */
encode(data: unknown): string | Uint8Array;
/** Decode data received from the wire */
decode(data: string | ArrayBuffer | Uint8Array): unknown;
}
/**
* Default JSON encoder - used when no encoder is specified.
* This maintains backwards compatibility with existing behavior.
*/
export const jsonEncoder: Encoder = {
encode: (data) => JSON.stringify(data),
decode: (data) => {
if (typeof data !== 'string') {
throw new Error(
'jsonEncoder received binary data. JSON uses text frames. ' +
'Use a binary encoder for binary data.',
);
}
return JSON.parse(data);
},
};

1
node_modules/@trpc/server/src/http.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export * from './@trpc/server/http';

1
node_modules/@trpc/server/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export * from './@trpc/server';

View File

@@ -0,0 +1,55 @@
import { observable } from './observable';
import type { Observable, Observer } from './types';
export interface BehaviorSubject<TValue> extends Observable<TValue, never> {
observable: Observable<TValue, never>;
next: (value: TValue) => void;
get: () => TValue;
}
export interface ReadonlyBehaviorSubject<TValue>
extends Omit<BehaviorSubject<TValue>, 'next'> {}
/**
* @internal
* An observable that maintains and provides a "current value" to subscribers
* @see https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject
*/
export function behaviorSubject<TValue>(
initialValue: TValue,
): BehaviorSubject<TValue> {
let value: TValue = initialValue;
const observerList: Observer<TValue, never>[] = [];
const addObserver = (observer: Observer<TValue, never>) => {
if (value !== undefined) {
observer.next(value);
}
observerList.push(observer);
};
const removeObserver = (observer: Observer<TValue, never>) => {
observerList.splice(observerList.indexOf(observer), 1);
};
const obs = observable<TValue, never>((observer) => {
addObserver(observer);
return () => {
removeObserver(observer);
};
}) as BehaviorSubject<TValue>;
obs.next = (nextValue: TValue) => {
if (value === nextValue) {
return;
}
value = nextValue;
for (const observer of observerList) {
observer.next(nextValue);
}
};
obs.get = () => value;
return obs;
}

29
node_modules/@trpc/server/src/observable/index.ts generated vendored Normal file
View File

@@ -0,0 +1,29 @@
export {
isObservable,
observable,
observableToAsyncIterable,
observableToPromise,
type inferObservableValue,
} from './observable';
export {
distinctUntilChanged,
distinctUntilDeepChanged,
map,
share,
tap,
} from './operators';
export type {
Observable,
Observer,
TeardownLogic,
Unsubscribable,
UnsubscribeFn,
} from './types';
export {
behaviorSubject,
type BehaviorSubject,
type ReadonlyBehaviorSubject,
} from './behaviorSubject';

206
node_modules/@trpc/server/src/observable/observable.ts generated vendored Normal file
View File

@@ -0,0 +1,206 @@
import type { Result } from '../unstable-core-do-not-import';
import type {
Observable,
Observer,
OperatorFunction,
TeardownLogic,
UnaryFunction,
Unsubscribable,
} from './types';
/** @public */
export type inferObservableValue<TObservable> =
TObservable extends Observable<infer TValue, unknown> ? TValue : never;
/** @public */
export function isObservable(x: unknown): x is Observable<unknown, unknown> {
return typeof x === 'object' && x !== null && 'subscribe' in x;
}
/** @public */
export function observable<TValue, TError = unknown>(
subscribe: (observer: Observer<TValue, TError>) => TeardownLogic,
): Observable<TValue, TError> {
const self: Observable<TValue, TError> = {
subscribe(observer) {
let teardownRef: TeardownLogic | null = null;
let isDone = false;
let unsubscribed = false;
let teardownImmediately = false;
function unsubscribe() {
if (teardownRef === null) {
teardownImmediately = true;
return;
}
if (unsubscribed) {
return;
}
unsubscribed = true;
if (typeof teardownRef === 'function') {
teardownRef();
} else if (teardownRef) {
teardownRef.unsubscribe();
}
}
teardownRef = subscribe({
next(value) {
if (isDone) {
return;
}
observer.next?.(value);
},
error(err) {
if (isDone) {
return;
}
isDone = true;
observer.error?.(err);
unsubscribe();
},
complete() {
if (isDone) {
return;
}
isDone = true;
observer.complete?.();
unsubscribe();
},
});
if (teardownImmediately) {
unsubscribe();
}
return {
unsubscribe,
};
},
pipe(
...operations: OperatorFunction<any, any, any, any>[]
): Observable<any, any> {
return operations.reduce(pipeReducer, self);
},
};
return self;
}
function pipeReducer(prev: any, fn: UnaryFunction<any, any>) {
return fn(prev);
}
/** @internal */
export function observableToPromise<TValue>(
observable: Observable<TValue, unknown>,
) {
const ac = new AbortController();
const promise = new Promise<TValue>((resolve, reject) => {
let isDone = false;
function onDone() {
if (isDone) {
return;
}
isDone = true;
obs$.unsubscribe();
}
ac.signal.addEventListener('abort', () => {
reject(ac.signal.reason);
});
const obs$ = observable.subscribe({
next(data) {
isDone = true;
resolve(data);
onDone();
},
error(data) {
reject(data);
},
complete() {
ac.abort();
onDone();
},
});
});
return promise;
}
/**
* @internal
*/
function observableToReadableStream<TValue>(
observable: Observable<TValue, unknown>,
signal: AbortSignal,
): ReadableStream<Result<TValue>> {
let unsub: Unsubscribable | null = null;
const onAbort = () => {
unsub?.unsubscribe();
unsub = null;
signal.removeEventListener('abort', onAbort);
};
return new ReadableStream<Result<TValue>>({
start(controller) {
unsub = observable.subscribe({
next(data) {
controller.enqueue({ ok: true, value: data });
},
error(error) {
controller.enqueue({ ok: false, error });
controller.close();
},
complete() {
controller.close();
},
});
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener('abort', onAbort, { once: true });
}
},
cancel() {
onAbort();
},
});
}
/** @internal */
export function observableToAsyncIterable<TValue>(
observable: Observable<TValue, unknown>,
signal: AbortSignal,
): AsyncIterable<TValue> {
const stream = observableToReadableStream(observable, signal);
const reader = stream.getReader();
const iterator: AsyncIterator<TValue> = {
async next() {
const value = await reader.read();
if (value.done) {
return {
value: undefined,
done: true,
};
}
const { value: result } = value;
if (!result.ok) {
throw result.error;
}
return {
value: result.value,
done: false,
};
},
async return() {
await reader.cancel();
return {
value: undefined,
done: true,
};
},
};
return {
[Symbol.asyncIterator]() {
return iterator;
},
};
}

161
node_modules/@trpc/server/src/observable/operators.ts generated vendored Normal file
View File

@@ -0,0 +1,161 @@
import { observable } from './observable';
import type {
MonoTypeOperatorFunction,
Observer,
OperatorFunction,
Unsubscribable,
} from './types';
export function map<TValueBefore, TError, TValueAfter>(
project: (value: TValueBefore, index: number) => TValueAfter,
): OperatorFunction<TValueBefore, TError, TValueAfter, TError> {
return (source) => {
return observable((destination) => {
let index = 0;
const subscription = source.subscribe({
next(value) {
destination.next(project(value, index++));
},
error(error) {
destination.error(error);
},
complete() {
destination.complete();
},
});
return subscription;
});
};
}
interface ShareConfig {}
export function share<TValue, TError>(
_opts?: ShareConfig,
): MonoTypeOperatorFunction<TValue, TError> {
return (source) => {
let refCount = 0;
let subscription: Unsubscribable | null = null;
const observers: Partial<Observer<TValue, TError>>[] = [];
function startIfNeeded() {
if (subscription) {
return;
}
subscription = source.subscribe({
next(value) {
for (const observer of observers) {
observer.next?.(value);
}
},
error(error) {
for (const observer of observers) {
observer.error?.(error);
}
},
complete() {
for (const observer of observers) {
observer.complete?.();
}
},
});
}
function resetIfNeeded() {
// "resetOnRefCountZero"
if (refCount === 0 && subscription) {
const _sub = subscription;
subscription = null;
_sub.unsubscribe();
}
}
return observable((subscriber) => {
refCount++;
observers.push(subscriber);
startIfNeeded();
return {
unsubscribe() {
refCount--;
resetIfNeeded();
const index = observers.findIndex((v) => v === subscriber);
if (index > -1) {
observers.splice(index, 1);
}
},
};
});
};
}
export function tap<TValue, TError>(
observer: Partial<Observer<TValue, TError>>,
): MonoTypeOperatorFunction<TValue, TError> {
return (source) => {
return observable((destination) => {
return source.subscribe({
next(value) {
observer.next?.(value);
destination.next(value);
},
error(error) {
observer.error?.(error);
destination.error(error);
},
complete() {
observer.complete?.();
destination.complete();
},
});
});
};
}
const distinctUnsetMarker = Symbol();
export function distinctUntilChanged<TValue, TError>(
compare: (a: TValue, b: TValue) => boolean = (a, b) => a === b,
): MonoTypeOperatorFunction<TValue, TError> {
return (source) => {
return observable((destination) => {
let lastValue: TValue | typeof distinctUnsetMarker = distinctUnsetMarker;
return source.subscribe({
next(value) {
if (lastValue !== distinctUnsetMarker && compare(lastValue, value)) {
return;
}
lastValue = value;
destination.next(value);
},
error(error) {
destination.error(error);
},
complete() {
destination.complete();
},
});
});
};
}
const isDeepEqual = <T>(a: T, b: T): boolean => {
if (a === b) {
return true;
}
const bothAreObjects =
a && b && typeof a === 'object' && typeof b === 'object';
return (
!!bothAreObjects &&
Object.keys(a).length === Object.keys(b).length &&
Object.entries(a).every(([k, v]) => isDeepEqual(v, b[k as keyof T]))
);
};
export function distinctUntilDeepChanged<
TValue,
TError,
>(): MonoTypeOperatorFunction<TValue, TError> {
return distinctUntilChanged(isDeepEqual);
}

76
node_modules/@trpc/server/src/observable/types.ts generated vendored Normal file
View File

@@ -0,0 +1,76 @@
export interface Unsubscribable {
unsubscribe(): void;
}
export type UnsubscribeFn = () => void;
interface Subscribable<TValue, TError> {
subscribe(observer: Partial<Observer<TValue, TError>>): Unsubscribable;
}
export interface Observable<TValue, TError>
extends Subscribable<TValue, TError> {
pipe(): Observable<TValue, TError>;
pipe<TValue1, TError1>(
op1: OperatorFunction<TValue, TError, TValue1, TError1>,
): Observable<TValue1, TError1>;
pipe<TValue1, TError1, TValue2, TError2>(
op1: OperatorFunction<TValue, TError, TValue1, TError1>,
op2: OperatorFunction<TValue1, TError1, TValue2, TError2>,
): Observable<TValue2, TError2>;
pipe<TValue1, TError1, TValue2, TError2, TValue3, TError3>(
op1: OperatorFunction<TValue, TError, TValue1, TError1>,
op2: OperatorFunction<TValue1, TError1, TValue2, TError2>,
op3: OperatorFunction<TValue2, TError2, TValue3, TError3>,
): Observable<TValue2, TError2>;
pipe<TValue1, TError1, TValue2, TError2, TValue3, TError3, TValue4, TError4>(
op1: OperatorFunction<TValue, TError, TValue1, TError1>,
op2: OperatorFunction<TValue1, TError1, TValue2, TError2>,
op3: OperatorFunction<TValue2, TError2, TValue3, TError3>,
op4: OperatorFunction<TValue3, TError3, TValue4, TError4>,
): Observable<TValue2, TError2>;
pipe<
TValue1,
TError1,
TValue2,
TError2,
TValue3,
TError3,
TValue4,
TError4,
TValue5,
TError5,
>(
op1: OperatorFunction<TValue, TError, TValue1, TError1>,
op2: OperatorFunction<TValue1, TError1, TValue2, TError2>,
op3: OperatorFunction<TValue2, TError2, TValue3, TError3>,
op4: OperatorFunction<TValue3, TError3, TValue4, TError4>,
op5: OperatorFunction<TValue4, TError4, TValue5, TError5>,
): Observable<TValue2, TError2>;
}
export interface Observer<TValue, TError> {
next: (value: TValue) => void;
error: (err: TError) => void;
complete: () => void;
}
export type TeardownLogic =
// | SubscriptionLike
Unsubscribable | UnsubscribeFn | void;
export type UnaryFunction<TSource, TReturn> = (source: TSource) => TReturn;
export type OperatorFunction<
TValueBefore,
TErrorBefore,
TValueAfter,
TErrorAfter,
> = UnaryFunction<
Subscribable<TValueBefore, TErrorBefore>,
Subscribable<TValueAfter, TErrorAfter>
>;
export type MonoTypeOperatorFunction<TValue, TError> = OperatorFunction<
TValue,
TError,
TValue,
TError
>;

1
node_modules/@trpc/server/src/rpc.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export * from './@trpc/server/rpc';

26
node_modules/@trpc/server/src/shared.ts generated vendored Normal file
View File

@@ -0,0 +1,26 @@
export {
/**
* @deprecated `import from '@trpc/server'` instead
*/
type inferProcedureInput,
/**
* @deprecated `import from '@trpc/server'` instead
*/
type inferProcedureOutput,
/**
* @deprecated `import from '@trpc/server'` instead
*/
type inferTransformedProcedureOutput,
/**
* @deprecated `import from '@trpc/server'` instead
*/
type inferTransformedSubscriptionOutput,
/**
* @deprecated `import from '@trpc/server'` instead
*/
getErrorShape,
/**
* @deprecated `import { createTRPCFlatProxy } from '@trpc/server'` instead
*/
createTRPCFlatProxy as createFlatProxy,
} from './@trpc/server';

View File

@@ -0,0 +1,46 @@
/**
* **DO NOT IMPORT FROM THIS FILE**
*
* This file is here to:
* - make TypeScript happy and prevent _"The inferred type of 'createContext' cannot be named without a reference to [...]"_.
* - the the glue between the official `@trpc/*`-packages
*
*
* If you seem to need to import anything from here, please open an issue at https://github.com/trpc/trpc/issues
*/
export * from './unstable-core-do-not-import/clientish/inference';
export * from './unstable-core-do-not-import/clientish/inferrable';
export * from './unstable-core-do-not-import/clientish/serialize';
export * from './unstable-core-do-not-import/createProxy';
export * from './unstable-core-do-not-import/error/formatter';
export * from './unstable-core-do-not-import/error/getErrorShape';
export * from './unstable-core-do-not-import/error/TRPCError';
export * from './unstable-core-do-not-import/http/contentType';
export * from './unstable-core-do-not-import/http/contentTypeParsers';
export * from './unstable-core-do-not-import/http/formDataToObject';
export * from './unstable-core-do-not-import/http/getHTTPStatusCode';
export * from './unstable-core-do-not-import/http/abortError';
export * from './unstable-core-do-not-import/http/parseConnectionParams';
export * from './unstable-core-do-not-import/http/resolveResponse';
export * from './unstable-core-do-not-import/http/types';
export * from './unstable-core-do-not-import/initTRPC';
export * from './unstable-core-do-not-import/middleware';
export * from './unstable-core-do-not-import/parser';
export * from './unstable-core-do-not-import/procedure';
export * from './unstable-core-do-not-import/procedureBuilder';
export * from './unstable-core-do-not-import/rootConfig';
export * from './unstable-core-do-not-import/router';
export * from './unstable-core-do-not-import/rpc';
export * from './unstable-core-do-not-import/stream/jsonl';
export * from './unstable-core-do-not-import/stream/sse.types';
export * from './unstable-core-do-not-import/stream/sse';
export * from './unstable-core-do-not-import/stream/tracked';
export * from './unstable-core-do-not-import/stream/utils/createDeferred';
export * from './unstable-core-do-not-import/stream/utils/disposable';
export * from './unstable-core-do-not-import/stream/utils/asyncIterable';
export * from './unstable-core-do-not-import/transformer';
export * from './unstable-core-do-not-import/types';
export * from './unstable-core-do-not-import/utils';
export * from './vendor/standard-schema-v1/error';
export * from './vendor/standard-schema-v1/spec';
export * from './vendor/unpromise';

View File

@@ -0,0 +1,60 @@
import type { inferObservableValue } from '../../observable';
import type {
AnyProcedure,
inferProcedureInput,
inferProcedureOutput,
} from '../procedure';
import type { AnyRouter, RouterRecord } from '../router';
import type {
AnyClientTypes,
inferClientTypes,
InferrableClientTypes,
} from './inferrable';
import type { Serialize } from './serialize';
/**
* @internal
*/
export type inferTransformedProcedureOutput<
TInferrable extends InferrableClientTypes,
TProcedure extends AnyProcedure,
> = inferClientTypes<TInferrable>['transformer'] extends false
? Serialize<inferProcedureOutput<TProcedure>>
: inferProcedureOutput<TProcedure>;
/** @internal */
export type inferTransformedSubscriptionOutput<
TInferrable extends InferrableClientTypes,
TProcedure extends AnyProcedure,
> = inferClientTypes<TInferrable>['transformer'] extends false
? Serialize<inferObservableValue<inferProcedureOutput<TProcedure>>>
: inferObservableValue<inferProcedureOutput<TProcedure>>;
export type GetInferenceHelpers<
TType extends 'input' | 'output',
TRoot extends AnyClientTypes,
TRecord extends RouterRecord,
> = {
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value
? $Value extends AnyProcedure
? TType extends 'input'
? inferProcedureInput<$Value>
: inferTransformedProcedureOutput<TRoot, $Value>
: $Value extends RouterRecord
? GetInferenceHelpers<TType, TRoot, $Value>
: never
: never;
};
export type inferRouterInputs<TRouter extends AnyRouter> = GetInferenceHelpers<
'input',
TRouter['_def']['_config']['$types'],
TRouter['_def']['record']
>;
export type inferRouterOutputs<TRouter extends AnyRouter> = GetInferenceHelpers<
'output',
TRouter['_def']['_config']['$types'],
TRouter['_def']['record']
>;

View File

@@ -0,0 +1,53 @@
import type { AnyRootTypes } from '../rootConfig';
export type AnyClientTypes = Pick<AnyRootTypes, 'errorShape' | 'transformer'>;
/**
* Result of `initTRPC.create()`
*/
type InitLike = {
_config: {
$types: AnyClientTypes;
};
};
/**
* Result of `initTRPC.create().router()`
*/
type RouterLike = {
_def: InitLike;
};
/**
* Result of `initTRPC.create()._config`
*/
type RootConfigLike = {
$types: AnyClientTypes;
};
/**
* Anything that can be inferred to the root config types needed for a TRPC client
*/
export type InferrableClientTypes =
| RouterLike
| InitLike
| RootConfigLike
| AnyClientTypes;
type PickTypes<T extends AnyClientTypes> = {
transformer: T['transformer'];
errorShape: T['errorShape'];
};
/**
* Infer the root types from a InferrableClientTypes
*/
export type inferClientTypes<TInferrable extends InferrableClientTypes> =
TInferrable extends AnyClientTypes
? PickTypes<TInferrable>
: TInferrable extends RootConfigLike
? PickTypes<TInferrable['$types']>
: TInferrable extends InitLike
? PickTypes<TInferrable['_config']['$types']>
: TInferrable extends RouterLike
? PickTypes<TInferrable['_def']['_config']['$types']>
: never;

View File

@@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { Simplify, WithoutIndexSignature } from '../types';
/**
* @see https://github.com/remix-run/remix/blob/2248669ed59fd716e267ea41df5d665d4781f4a9/packages/remix-server-runtime/serialize.ts
*/
type JsonPrimitive = boolean | number | string | null;
type JsonArray = JsonValue[] | readonly JsonValue[];
type JsonObject = {
readonly [key: string | number]: JsonValue;
[key: symbol]: never;
};
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type IsJson<T> = T extends JsonValue ? true : false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
type NonJsonPrimitive = Function | symbol | undefined;
/*
* `any` is the only type that can let you equate `0` with `1`
* See https://stackoverflow.com/a/49928360/1490091
*/
type IsAny<T> = 0 extends T & 1 ? true : false;
// `undefined` is a weird one that's technically not valid JSON,
// but the return value of `JSON.parse` can be `undefined` so we
// support it as both a Primitive and a NonJsonPrimitive
type JsonReturnable = JsonPrimitive | undefined;
type IsRecord<T extends object> = keyof WithoutIndexSignature<T> extends never
? true
: false;
/* prettier-ignore */
export type Serialize<T> =
IsAny<T> extends true ? any :
unknown extends T ? unknown :
IsJson<T> extends true ? T :
T extends AsyncIterable<infer $T, infer $Return, infer $Next> ? AsyncIterable<Serialize<$T>, Serialize<$Return>, Serialize<$Next>> :
T extends PromiseLike<infer $T> ? Promise<Serialize<$T>> :
T extends JsonReturnable ? T :
T extends Map<any, any> | Set<any> ? object :
T extends NonJsonPrimitive ? never :
T extends { toJSON(): infer U } ? U :
T extends [] ? [] :
T extends [unknown, ...unknown[]] ? SerializeTuple<T> :
T extends readonly (infer U)[] ? (U extends NonJsonPrimitive ? null : Serialize<U>)[] :
T extends object ?
IsRecord<T> extends true ? Record<keyof T, Serialize<T[keyof T]>> :
Simplify<SerializeObject<UndefinedToOptional<T>>> :
never;
/** JSON serialize [tuples](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) */
type SerializeTuple<T extends [unknown, ...unknown[]]> = {
[K in keyof T]: T[K] extends NonJsonPrimitive ? null : Serialize<T[K]>;
};
// prettier-ignore
type SerializeObjectKey<T extends Record<any, any>, K> =
// never include entries where the key is a symbol
K extends symbol ? never :
// always include entries where the value is any
IsAny<T[K]> extends true ? K :
// always include entries where the value is unknown
unknown extends T[K] ? K :
// never include entries where the value is a non-JSON primitive
T[K] extends NonJsonPrimitive ? never :
// otherwise serialize the value
K;
/**
* JSON serialize objects (not including arrays) and classes
* @internal
**/
export type SerializeObject<T extends object> = {
[K in keyof T as SerializeObjectKey<T, K>]: Serialize<T[K]>;
};
/**
* Extract keys from T where the value dosen't extend undefined
* Note: Can't parse IndexSignature or Record types
*/
type FilterDefinedKeys<T extends object> = Exclude<
{
[K in keyof T]: undefined extends T[K] ? never : K;
}[keyof T],
undefined
>;
/**
* Get value of exactOptionalPropertyTypes config
*/
type ExactOptionalPropertyTypes = { a?: 0 | undefined } extends {
a?: 0;
}
? false
: true;
/**
* Check if T has an index signature
*/
type HasIndexSignature<T extends object> = string extends keyof T
? true
: false;
/**
* { [key: string]: number | undefined } --> { [key: string]: number }
*/
type HandleIndexSignature<T extends object> = {
[K in keyof Omit<T, keyof WithoutIndexSignature<T>>]: Exclude<
T[K],
undefined
>;
};
/**
* { a: number | undefined } --> { a?: number }
* Note: Can't parse IndexSignature or Record types
*/
type HandleUndefined<T extends object> = {
[K in keyof Omit<T, FilterDefinedKeys<T>>]?: Exclude<T[K], undefined>;
};
/**
* Handle undefined, index signature and records
*/
type UndefinedToOptional<T extends object> =
// Property is not a union with `undefined`, keep as-is
Pick<WithoutIndexSignature<T>, FilterDefinedKeys<WithoutIndexSignature<T>>> &
// If following is true, don't merge undefined or optional into index signature if any in T
(ExactOptionalPropertyTypes extends true
? HandleIndexSignature<T> & HandleUndefined<WithoutIndexSignature<T>>
: HasIndexSignature<T> extends true
? HandleIndexSignature<T>
: HandleUndefined<T>);

View File

@@ -0,0 +1,87 @@
import { emptyObject } from './utils';
interface ProxyCallbackOptions {
path: readonly string[];
args: readonly unknown[];
}
type ProxyCallback = (opts: ProxyCallbackOptions) => unknown;
const noop = () => {
// noop
};
const freezeIfAvailable = (obj: object) => {
if (Object.freeze) {
Object.freeze(obj);
}
};
function createInnerProxy(
callback: ProxyCallback,
path: readonly string[],
memo: Record<string, unknown>,
) {
const cacheKey = path.join('.');
memo[cacheKey] ??= new Proxy(noop, {
get(_obj, key) {
if (typeof key !== 'string' || key === 'then') {
// special case for if the proxy is accidentally treated
// like a PromiseLike (like in `Promise.resolve(proxy)`)
return undefined;
}
return createInnerProxy(callback, [...path, key], memo);
},
apply(_1, _2, args) {
const lastOfPath = path[path.length - 1];
let opts = { args, path };
// special handling for e.g. `trpc.hello.call(this, 'there')` and `trpc.hello.apply(this, ['there'])
if (lastOfPath === 'call') {
opts = {
args: args.length >= 2 ? [args[1]] : [],
path: path.slice(0, -1),
};
} else if (lastOfPath === 'apply') {
opts = {
args: args.length >= 2 ? args[1] : [],
path: path.slice(0, -1),
};
}
freezeIfAvailable(opts.args);
freezeIfAvailable(opts.path);
return callback(opts);
},
});
return memo[cacheKey];
}
/**
* Creates a proxy that calls the callback with the path and arguments
*
* @internal
*/
export const createRecursiveProxy = <TFaux = unknown>(
callback: ProxyCallback,
): TFaux => createInnerProxy(callback, [], emptyObject()) as TFaux;
/**
* Used in place of `new Proxy` where each handler will map 1 level deep to another value.
*
* @internal
*/
export const createFlatProxy = <TFaux>(
callback: (path: keyof TFaux) => any,
): TFaux => {
return new Proxy(noop, {
get(_obj, name) {
if (name === 'then') {
// special case for if the proxy is accidentally treated
// like a PromiseLike (like in `Promise.resolve(proxy)`)
return undefined;
}
return callback(name as any);
},
}) as TFaux;
};

View File

@@ -0,0 +1,87 @@
import type { TRPC_ERROR_CODE_KEY } from '../rpc/codes';
import { isObject } from '../utils';
class UnknownCauseError extends Error {
[key: string]: unknown;
constructor(cause: object) {
super(getMessage(cause));
Object.assign(this, cause);
}
}
function getMessage(cause: object) {
if ('message' in cause) return String(cause.message);
return undefined;
}
export function getCauseFromUnknown(cause: unknown): Error | undefined {
if (cause instanceof Error) {
return cause;
}
const type = typeof cause;
if (type === 'undefined' || type === 'function' || cause === null) {
return undefined;
}
// Primitive types just get wrapped in an error
if (type !== 'object') {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
return new Error(String(cause));
}
// If it's an object, we'll create a synthetic error
if (isObject(cause)) {
return new UnknownCauseError(cause);
}
return undefined;
}
export function getTRPCErrorFromUnknown(cause: unknown): TRPCError {
if (cause instanceof TRPCError) {
return cause;
}
if (cause instanceof Error && cause.name === 'TRPCError') {
// https://github.com/trpc/trpc/pull/4848
return cause as TRPCError;
}
const trpcError = new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
cause,
});
// Inherit stack from error
if (cause instanceof Error && cause.stack) {
trpcError.stack = cause.stack;
}
return trpcError;
}
export class TRPCError extends Error {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore override doesn't work in all environments due to "This member cannot have an 'override' modifier because it is not declared in the base class 'Error'"
public override readonly cause?: Error;
public readonly code;
constructor(opts: {
message?: string;
code: TRPC_ERROR_CODE_KEY;
cause?: unknown;
}) {
const cause = getCauseFromUnknown(opts.cause);
const message = opts.message ?? cause?.message ?? opts.code;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore https://github.com/tc39/proposal-error-cause
super(message, { cause });
this.code = opts.code;
this.name = 'TRPCError';
this.cause ??= cause;
}
}

View File

@@ -0,0 +1,47 @@
import type { ProcedureType } from '../procedure';
import type {
TRPC_ERROR_CODE_KEY,
TRPC_ERROR_CODE_NUMBER,
TRPCErrorShape,
} from '../rpc';
import type { TRPCError } from './TRPCError';
/**
* @internal
*/
export type ErrorFormatter<TContext, TShape extends TRPCErrorShape> = (opts: {
error: TRPCError;
type: ProcedureType | 'unknown';
path: string | undefined;
input: unknown;
ctx: TContext | undefined;
shape: DefaultErrorShape;
}) => TShape;
/**
* @internal
*/
export type DefaultErrorData = {
code: TRPC_ERROR_CODE_KEY;
httpStatus: number;
/**
* Path to the procedure that threw the error
*/
path?: string;
/**
* Stack trace of the error (only in development)
*/
stack?: string;
};
/**
* @internal
*/
export interface DefaultErrorShape extends TRPCErrorShape<DefaultErrorData> {
message: string;
code: TRPC_ERROR_CODE_NUMBER;
}
export const defaultFormatter: ErrorFormatter<any, any> = ({ shape }) => {
return shape;
};

View File

@@ -0,0 +1,36 @@
import { getHTTPStatusCodeFromError } from '../http/getHTTPStatusCode';
import type { ProcedureType } from '../procedure';
import type { AnyRootTypes, RootConfig } from '../rootConfig';
import { TRPC_ERROR_CODES_BY_KEY } from '../rpc';
import type { DefaultErrorShape } from './formatter';
import type { TRPCError } from './TRPCError';
/**
* @internal
*/
export function getErrorShape<TRoot extends AnyRootTypes>(opts: {
config: RootConfig<TRoot>;
error: TRPCError;
type: ProcedureType | 'unknown';
path: string | undefined;
input: unknown;
ctx: TRoot['ctx'] | undefined;
}): TRoot['errorShape'] {
const { path, error, config } = opts;
const { code } = opts.error;
const shape: DefaultErrorShape = {
message: error.message,
code: TRPC_ERROR_CODES_BY_KEY[code],
data: {
code,
httpStatus: getHTTPStatusCodeFromError(error),
},
};
if (config.isDev && typeof opts.error.stack === 'string') {
shape.data.stack = opts.error.stack;
}
if (typeof path === 'string') {
shape.data.path = path;
}
return config.errorFormatter({ ...opts, shape });
}

View File

@@ -0,0 +1,11 @@
import { isObject } from '../utils';
export function isAbortError(
error: unknown,
): error is DOMException | Error | { name: 'AbortError' } {
return isObject(error) && error['name'] === 'AbortError';
}
export function throwAbortError(message = 'AbortError'): never {
throw new DOMException(message, 'AbortError');
}

View File

@@ -0,0 +1,320 @@
import { TRPCError } from '../error/TRPCError';
import type { ProcedureType } from '../procedure';
import { getProcedureAtPath, type AnyRouter } from '../router';
import { emptyObject, isObject } from '../utils';
import { parseConnectionParamsFromString } from './parseConnectionParams';
import type { TRPCAcceptHeader, TRPCRequestInfo } from './types';
export function getAcceptHeader(headers: Headers): TRPCAcceptHeader | null {
return (
(headers.get('trpc-accept') as TRPCAcceptHeader | null) ??
(headers
.get('accept')
?.split(',')
.some((t) => t.trim() === 'application/jsonl')
? ('application/jsonl' as TRPCAcceptHeader)
: null)
);
}
type GetRequestInfoOptions = {
path: string;
req: Request;
url: URL | null;
searchParams: URLSearchParams;
headers: Headers;
router: AnyRouter;
maxBatchSize?: number;
};
type ContentTypeHandler = {
isMatch: (opts: Request) => boolean;
parse: (opts: GetRequestInfoOptions) => Promise<TRPCRequestInfo>;
};
/**
* Memoize a function that takes no arguments
* @internal
*/
function memo<TReturn>(fn: () => Promise<TReturn>) {
let promise: Promise<TReturn> | null = null;
const sym = Symbol.for('@trpc/server/http/memo');
let value: TReturn | typeof sym = sym;
return {
/**
* Lazily read the value
*/
read: async (): Promise<TReturn> => {
if (value !== sym) {
return value;
}
// dedupes promises and catches errors
promise ??= fn().catch((cause) => {
if (cause instanceof TRPCError) {
throw cause;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: cause instanceof Error ? cause.message : 'Invalid input',
cause,
});
});
value = await promise;
promise = null;
return value;
},
/**
* Get an already stored result
*/
result: (): TReturn | undefined => {
return value !== sym ? value : undefined;
},
};
}
const jsonContentTypeHandler: ContentTypeHandler = {
isMatch(req) {
return !!req.headers.get('content-type')?.startsWith('application/json');
},
async parse(opts) {
const { req } = opts;
const isBatchCall = opts.searchParams.get('batch') === '1';
const maxBatchSize = opts.maxBatchSize;
const paths = isBatchCall ? opts.path.split(',') : [opts.path];
if (
isBatchCall &&
typeof maxBatchSize === 'number' &&
paths.length > maxBatchSize
) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Batch call exceeds maximum size`,
});
}
type InputRecord = Record<number, unknown>;
const getInputs = memo(async (): Promise<InputRecord> => {
let inputs: unknown = undefined;
if (req.method === 'GET') {
const queryInput = opts.searchParams.get('input');
if (queryInput) {
inputs = JSON.parse(queryInput);
}
} else {
inputs = await req.json();
}
if (inputs === undefined) {
return emptyObject();
}
if (!isBatchCall) {
const result: InputRecord = emptyObject();
result[0] =
opts.router._def._config.transformer.input.deserialize(inputs);
return result;
}
if (!isObject(inputs)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: '"input" needs to be an object when doing a batch call',
});
}
const acc: InputRecord = emptyObject();
for (const index of paths.keys()) {
const input = inputs[index];
if (input !== undefined) {
acc[index] =
opts.router._def._config.transformer.input.deserialize(input);
}
}
return acc;
});
const calls = await Promise.all(
paths.map(
async (path, index): Promise<TRPCRequestInfo['calls'][number]> => {
const procedure = await getProcedureAtPath(opts.router, path);
return {
batchIndex: index,
path,
procedure,
getRawInput: async () => {
const inputs = await getInputs.read();
let input = inputs[index];
if (procedure?._def.type === 'subscription') {
const lastEventId =
opts.headers.get('last-event-id') ??
opts.searchParams.get('lastEventId') ??
opts.searchParams.get('Last-Event-Id');
if (lastEventId) {
if (isObject(input)) {
input = {
...input,
lastEventId: lastEventId,
};
} else {
input ??= {
lastEventId: lastEventId,
};
}
}
}
return input;
},
result: () => {
return getInputs.result()?.[index];
},
};
},
),
);
const types = new Set(
calls.map((call) => call.procedure?._def.type).filter(Boolean),
);
/* istanbul ignore if -- @preserve */
if (types.size > 1) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Cannot mix procedure types in call: ${Array.from(types).join(
', ',
)}`,
});
}
const type: ProcedureType | 'unknown' =
types.values().next().value ?? 'unknown';
const connectionParamsStr = opts.searchParams.get('connectionParams');
const info: TRPCRequestInfo = {
isBatchCall,
accept: getAcceptHeader(req.headers),
calls,
type,
connectionParams:
connectionParamsStr === null
? null
: parseConnectionParamsFromString(connectionParamsStr),
signal: req.signal,
url: opts.url,
};
return info;
},
};
const formDataContentTypeHandler: ContentTypeHandler = {
isMatch(req) {
return !!req.headers.get('content-type')?.startsWith('multipart/form-data');
},
async parse(opts) {
const { req } = opts;
if (req.method !== 'POST') {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message:
'Only POST requests are supported for multipart/form-data requests',
});
}
const getInputs = memo(async () => {
const fd = await req.formData();
return fd;
});
const procedure = await getProcedureAtPath(opts.router, opts.path);
return {
accept: null,
calls: [
{
batchIndex: 0,
path: opts.path,
getRawInput: getInputs.read,
result: getInputs.result,
procedure,
},
],
isBatchCall: false,
type: 'mutation',
connectionParams: null,
signal: req.signal,
url: opts.url,
};
},
};
const octetStreamContentTypeHandler: ContentTypeHandler = {
isMatch(req) {
return !!req.headers
.get('content-type')
?.startsWith('application/octet-stream');
},
async parse(opts) {
const { req } = opts;
if (req.method !== 'POST') {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message:
'Only POST requests are supported for application/octet-stream requests',
});
}
const getInputs = memo(async () => {
return req.body;
});
return {
calls: [
{
batchIndex: 0,
path: opts.path,
getRawInput: getInputs.read,
result: getInputs.result,
procedure: await getProcedureAtPath(opts.router, opts.path),
},
],
isBatchCall: false,
accept: null,
type: 'mutation',
connectionParams: null,
signal: req.signal,
url: opts.url,
};
},
};
const handlers = [
jsonContentTypeHandler,
formDataContentTypeHandler,
octetStreamContentTypeHandler,
];
function getContentTypeHandler(req: Request): ContentTypeHandler {
const handler = handlers.find((handler) => handler.isMatch(req));
if (handler) {
return handler;
}
if (!handler && req.method === 'GET') {
// fallback to JSON for get requests so GET-requests can be opened in browser easily
return jsonContentTypeHandler;
}
throw new TRPCError({
code: 'UNSUPPORTED_MEDIA_TYPE',
message: req.headers.has('content-type')
? `Unsupported content-type "${req.headers.get('content-type')}`
: 'Missing content-type header',
});
}
export async function getRequestInfo(
opts: GetRequestInfoOptions,
): Promise<TRPCRequestInfo> {
const handler = getContentTypeHandler(opts.req);
return await handler.parse(opts);
}

View File

@@ -0,0 +1,38 @@
import type { ParserZodEsque } from '../parser';
/**
* @internal
*/
export type UtilityParser<TInput, TOutput> = ParserZodEsque<TInput, TOutput> & {
parse: (input: unknown) => TOutput;
};
// Should be the same possible types as packages/client/src/links/internals/contentTypes.ts isOctetType
/**
* @internal
*
* File is only available from Node19+ but it always extends Blob so we can use that as a type until we eventually drop Node18
*/
export interface FileLike extends Blob {
readonly name: string;
}
/**
* @internal
*/
export type OctetInput = Blob | Uint8Array | FileLike;
export const octetInputParser: UtilityParser<OctetInput, ReadableStream> = {
_input: null as any as OctetInput,
_output: null as any as ReadableStream,
parse(input) {
if (input instanceof ReadableStream) {
return input;
}
throw new Error(
`Parsed input was expected to be a ReadableStream but was: ${typeof input}`,
);
},
};

View File

@@ -0,0 +1,47 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { emptyObject } from '../utils';
const isNumberString = (str: string) => /^\d+$/.test(str);
function set(
obj: Record<string, any>,
path: readonly string[],
value: unknown,
): void {
if (path.length > 1) {
const newPath = [...path];
const key = newPath.shift()!;
const nextKey = newPath[0]!;
if (!Object.hasOwn(obj, key)) {
obj[key] = isNumberString(nextKey) ? [] : emptyObject();
} else if (Array.isArray(obj[key]) && !isNumberString(nextKey)) {
obj[key] = Object.fromEntries(Object.entries(obj[key]));
}
set(obj[key], newPath, value);
return;
}
const p = path[0]!;
if (obj[p] === undefined) {
obj[p] = value;
} else if (Array.isArray(obj[p])) {
obj[p].push(value);
} else {
obj[p] = [obj[p], value];
}
}
export function formDataToObject(formData: FormData) {
const obj: Record<string, unknown> = emptyObject();
for (const [key, value] of formData.entries()) {
const parts = key.split(/[\.\[\]]/).filter(Boolean);
set(obj, parts, value);
}
return obj;
}

View File

@@ -0,0 +1,98 @@
import type { TRPCError } from '../error/TRPCError';
import type { TRPC_ERROR_CODES_BY_KEY, TRPCResponse } from '../rpc';
import { TRPC_ERROR_CODES_BY_NUMBER } from '../rpc';
import type { InvertKeyValue, ValueOf } from '../types';
import { isObject } from '../utils';
export const JSONRPC2_TO_HTTP_CODE: Record<
keyof typeof TRPC_ERROR_CODES_BY_KEY,
number
> = {
PARSE_ERROR: 400,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_SUPPORTED: 405,
TIMEOUT: 408,
CONFLICT: 409,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
UNSUPPORTED_MEDIA_TYPE: 415,
UNPROCESSABLE_CONTENT: 422,
PRECONDITION_REQUIRED: 428,
TOO_MANY_REQUESTS: 429,
CLIENT_CLOSED_REQUEST: 499,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
};
export const HTTP_CODE_TO_JSONRPC2: InvertKeyValue<
typeof JSONRPC2_TO_HTTP_CODE
> = {
400: 'BAD_REQUEST',
401: 'UNAUTHORIZED',
402: 'PAYMENT_REQUIRED',
403: 'FORBIDDEN',
404: 'NOT_FOUND',
405: 'METHOD_NOT_SUPPORTED',
408: 'TIMEOUT',
409: 'CONFLICT',
412: 'PRECONDITION_FAILED',
413: 'PAYLOAD_TOO_LARGE',
415: 'UNSUPPORTED_MEDIA_TYPE',
422: 'UNPROCESSABLE_CONTENT',
428: 'PRECONDITION_REQUIRED',
429: 'TOO_MANY_REQUESTS',
499: 'CLIENT_CLOSED_REQUEST',
500: 'INTERNAL_SERVER_ERROR',
501: 'NOT_IMPLEMENTED',
502: 'BAD_GATEWAY',
503: 'SERVICE_UNAVAILABLE',
504: 'GATEWAY_TIMEOUT',
} as const;
export function getStatusCodeFromKey(
code: keyof typeof TRPC_ERROR_CODES_BY_KEY,
) {
return JSONRPC2_TO_HTTP_CODE[code] ?? 500;
}
export function getStatusKeyFromCode(
code: keyof typeof HTTP_CODE_TO_JSONRPC2,
): ValueOf<typeof HTTP_CODE_TO_JSONRPC2> {
return HTTP_CODE_TO_JSONRPC2[code] ?? 'INTERNAL_SERVER_ERROR';
}
export function getHTTPStatusCode(json: TRPCResponse | TRPCResponse[]) {
const arr = Array.isArray(json) ? json : [json];
const httpStatuses = new Set<number>(
arr.map((res) => {
if ('error' in res && isObject(res.error.data)) {
if (typeof res.error.data?.['httpStatus'] === 'number') {
return res.error.data['httpStatus'];
}
const code = TRPC_ERROR_CODES_BY_NUMBER[res.error.code];
return getStatusCodeFromKey(code);
}
return 200;
}),
);
if (httpStatuses.size !== 1) {
return 207;
}
const httpStatus = httpStatuses.values().next().value;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return httpStatus!;
}
export function getHTTPStatusCodeFromError(error: TRPCError) {
return getStatusCodeFromKey(error.code);
}

View File

@@ -0,0 +1,49 @@
import { TRPCError } from '../error/TRPCError';
import { isObject } from '../utils';
import type { TRPCRequestInfo } from './types';
export function parseConnectionParamsFromUnknown(
parsed: unknown,
): TRPCRequestInfo['connectionParams'] {
try {
if (parsed === null) {
return null;
}
if (!isObject(parsed)) {
throw new Error('Expected object');
}
const nonStringValues = Object.entries(parsed).filter(
([_key, value]) => typeof value !== 'string',
);
if (nonStringValues.length > 0) {
throw new Error(
`Expected connectionParams to be string values. Got ${nonStringValues
.map(([key, value]) => `${key}: ${typeof value}`)
.join(', ')}`,
);
}
return parsed as Record<string, string>;
} catch (cause) {
throw new TRPCError({
code: 'PARSE_ERROR',
message: 'Invalid connection params shape',
cause,
});
}
}
export function parseConnectionParamsFromString(
str: string,
): TRPCRequestInfo['connectionParams'] {
let parsed: unknown;
try {
parsed = JSON.parse(str);
} catch (cause) {
throw new TRPCError({
code: 'PARSE_ERROR',
message: 'Not JSON-parsable query params',
cause,
});
}
return parseConnectionParamsFromUnknown(parsed);
}

View File

@@ -0,0 +1,775 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
isObservable,
observableToAsyncIterable,
} from '../../observable/observable';
import { getErrorShape } from '../error/getErrorShape';
import { getTRPCErrorFromUnknown, TRPCError } from '../error/TRPCError';
import type { ProcedureType } from '../procedure';
import {
type AnyRouter,
type inferRouterContext,
type inferRouterError,
} from '../router';
import type { TRPCResponse } from '../rpc';
import { isPromise, jsonlStreamProducer } from '../stream/jsonl';
import { sseHeaders, sseStreamProducer } from '../stream/sse';
import { transformTRPCResponse } from '../transformer';
import {
abortSignalsAnyPonyfill,
isAsyncIterable,
isObject,
run,
} from '../utils';
import { getAcceptHeader, getRequestInfo } from './contentType';
import { getHTTPStatusCode } from './getHTTPStatusCode';
import type {
HTTPBaseHandlerOptions,
ResolveHTTPRequestOptionsContextFn,
TRPCRequestInfo,
} from './types';
function errorToAsyncIterable(err: TRPCError): AsyncIterable<never> {
return run(async function* () {
throw err;
});
}
type HTTPMethods =
| 'GET'
| 'POST'
| 'HEAD'
| 'OPTIONS'
| 'PUT'
| 'DELETE'
| 'PATCH';
function combinedAbortController(signal: AbortSignal) {
const controller = new AbortController();
const combinedSignal = abortSignalsAnyPonyfill([signal, controller.signal]);
return {
signal: combinedSignal,
controller,
};
}
const TYPE_ACCEPTED_METHOD_MAP: Record<ProcedureType, HTTPMethods[]> = {
mutation: ['POST'],
query: ['GET'],
subscription: ['GET'],
};
const TYPE_ACCEPTED_METHOD_MAP_WITH_METHOD_OVERRIDE: Record<
ProcedureType,
HTTPMethods[]
> = {
// never allow GET to do a mutation
mutation: ['POST'],
query: ['GET', 'POST'],
subscription: ['GET', 'POST'],
};
interface ResolveHTTPRequestOptions<TRouter extends AnyRouter>
extends HTTPBaseHandlerOptions<TRouter, Request> {
createContext: ResolveHTTPRequestOptionsContextFn<TRouter>;
req: Request;
path: string;
/**
* If the request had an issue before reaching the handler
*/
error: TRPCError | null;
}
function initResponse<TRouter extends AnyRouter, TRequest>(initOpts: {
ctx: inferRouterContext<TRouter> | undefined;
info: TRPCRequestInfo | undefined;
responseMeta?: HTTPBaseHandlerOptions<TRouter, TRequest>['responseMeta'];
untransformedJSON:
| TRPCResponse<unknown, inferRouterError<TRouter>>
| TRPCResponse<unknown, inferRouterError<TRouter>>[]
| null;
errors: TRPCError[];
headers: Headers;
}) {
const {
ctx,
info,
responseMeta,
untransformedJSON,
errors = [],
headers,
} = initOpts;
let status = untransformedJSON ? getHTTPStatusCode(untransformedJSON) : 200;
const eagerGeneration = !untransformedJSON;
const data = eagerGeneration
? []
: Array.isArray(untransformedJSON)
? untransformedJSON
: [untransformedJSON];
const meta =
responseMeta?.({
ctx,
info,
paths: info?.calls.map((call) => call.path),
data,
errors,
eagerGeneration,
type:
info?.calls.find((call) => call.procedure?._def.type)?.procedure?._def
.type ?? 'unknown',
}) ?? {};
if (meta.headers) {
if (meta.headers instanceof Headers) {
for (const [key, value] of meta.headers.entries()) {
headers.append(key, value);
}
} else {
/**
* @deprecated, delete in v12
*/
for (const [key, value] of Object.entries(meta.headers)) {
if (Array.isArray(value)) {
for (const v of value) {
headers.append(key, v);
}
} else if (typeof value === 'string') {
headers.set(key, value);
}
}
}
}
if (meta.status) {
status = meta.status;
}
return {
status,
};
}
function caughtErrorToData<TRouter extends AnyRouter>(
cause: unknown,
errorOpts: {
opts: Pick<
ResolveHTTPRequestOptions<TRouter>,
'onError' | 'req' | 'router'
>;
ctx: inferRouterContext<TRouter> | undefined;
type: ProcedureType | 'unknown';
path?: string;
input?: unknown;
},
) {
const { router, req, onError } = errorOpts.opts;
const error = getTRPCErrorFromUnknown(cause);
onError?.({
error,
path: errorOpts.path,
input: errorOpts.input,
ctx: errorOpts.ctx,
type: errorOpts.type,
req,
});
const untransformedJSON = {
error: getErrorShape({
config: router._def._config,
error,
type: errorOpts.type,
path: errorOpts.path,
input: errorOpts.input,
ctx: errorOpts.ctx,
}),
};
const transformedJSON = transformTRPCResponse(
router._def._config,
untransformedJSON,
);
const body = JSON.stringify(transformedJSON);
return {
error,
untransformedJSON,
body,
};
}
/**
* Check if a value is a stream-like object
* - if it's an async iterable
* - if it's an object with async iterables or promises
*/
function isDataStream(v: unknown) {
if (!isObject(v)) {
return false;
}
if (isAsyncIterable(v)) {
return true;
}
return (
Object.values(v).some(isPromise) || Object.values(v).some(isAsyncIterable)
);
}
type ResultTuple<T> = [undefined, T] | [TRPCError, undefined];
export async function resolveResponse<TRouter extends AnyRouter>(
opts: ResolveHTTPRequestOptions<TRouter>,
): Promise<Response> {
const { router, req } = opts;
const headers = new Headers([['vary', 'trpc-accept, accept']]);
const config = router._def._config;
const url = new URL(req.url);
if (req.method === 'HEAD') {
// can be used for lambda warmup
return new Response(null, {
status: 204,
});
}
const allowBatching = opts.allowBatching ?? opts.batching?.enabled ?? true;
const allowMethodOverride =
(opts.allowMethodOverride ?? false) && req.method === 'POST';
type $Context = inferRouterContext<TRouter>;
const infoTuple: ResultTuple<TRPCRequestInfo> = await run(async () => {
try {
return [
undefined,
await getRequestInfo({
req,
path: decodeURIComponent(opts.path),
router,
searchParams: url.searchParams,
headers: opts.req.headers,
url,
maxBatchSize: opts.maxBatchSize,
}),
];
} catch (cause) {
return [getTRPCErrorFromUnknown(cause), undefined];
}
});
interface ContextManager {
valueOrUndefined: () => $Context | undefined;
value: () => $Context;
create: (info: TRPCRequestInfo) => Promise<void>;
}
const ctxManager: ContextManager = run(() => {
let result: ResultTuple<$Context> | undefined = undefined;
return {
valueOrUndefined: () => {
if (!result) {
return undefined;
}
return result[1];
},
value: () => {
const [err, ctx] = result!;
if (err) {
throw err;
}
return ctx;
},
create: async (info) => {
if (result) {
throw new Error(
'This should only be called once - report a bug in tRPC',
);
}
try {
const ctx = await opts.createContext({
info,
});
result = [undefined, ctx];
} catch (cause) {
result = [getTRPCErrorFromUnknown(cause), undefined];
}
},
};
});
const methodMapper = allowMethodOverride
? TYPE_ACCEPTED_METHOD_MAP_WITH_METHOD_OVERRIDE
: TYPE_ACCEPTED_METHOD_MAP;
/**
* @deprecated
*/
const isStreamCall = getAcceptHeader(req.headers) === 'application/jsonl';
const experimentalSSE = config.sse?.enabled ?? true;
try {
const [infoError, info] = infoTuple;
if (infoError) {
throw infoError;
}
if (info.isBatchCall && !allowBatching) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Batching is not enabled on the server`,
});
}
/* istanbul ignore if -- @preserve */
if (isStreamCall && !info.isBatchCall) {
throw new TRPCError({
message: `Streaming requests must be batched (you can do a batch of 1)`,
code: 'BAD_REQUEST',
});
}
await ctxManager.create(info);
interface RPCResultOk {
data: unknown;
signal?: AbortSignal;
}
type RPCResult = ResultTuple<RPCResultOk>;
const rpcCalls = info.calls.map(async (call): Promise<RPCResult> => {
const proc = call.procedure;
const combinedAbort = combinedAbortController(opts.req.signal);
try {
if (opts.error) {
throw opts.error;
}
if (!proc) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `No procedure found on path "${call.path}"`,
});
}
if (!methodMapper[proc._def.type].includes(req.method as HTTPMethods)) {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: `Unsupported ${req.method}-request to ${proc._def.type} procedure at path "${call.path}"`,
});
}
if (proc._def.type === 'subscription') {
/* istanbul ignore if -- @preserve */
if (info.isBatchCall) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Cannot batch subscription calls`,
});
}
if (config.sse?.maxDurationMs) {
function cleanup() {
clearTimeout(timer);
combinedAbort.signal.removeEventListener('abort', cleanup);
combinedAbort.controller.abort();
}
const timer = setTimeout(cleanup, config.sse.maxDurationMs);
combinedAbort.signal.addEventListener('abort', cleanup);
}
}
const data: unknown = await proc({
path: call.path,
getRawInput: call.getRawInput,
ctx: ctxManager.value(),
type: proc._def.type,
signal: combinedAbort.signal,
batchIndex: call.batchIndex,
});
return [
undefined,
{
data,
signal:
proc._def.type === 'subscription'
? combinedAbort.signal
: undefined,
},
];
} catch (cause) {
const error = getTRPCErrorFromUnknown(cause);
const input = call.result();
opts.onError?.({
error,
path: call.path,
input,
ctx: ctxManager.valueOrUndefined(),
type: call.procedure?._def.type ?? 'unknown',
req: opts.req,
});
return [error, undefined];
}
});
// ----------- response handlers -----------
if (!info.isBatchCall) {
const [call] = info.calls;
const [error, result] = await rpcCalls[0]!;
switch (info.type) {
case 'unknown':
case 'mutation':
case 'query': {
// httpLink
headers.set('content-type', 'application/json');
if (isDataStream(result?.data)) {
throw new TRPCError({
code: 'UNSUPPORTED_MEDIA_TYPE',
message:
'Cannot use stream-like response in non-streaming request - use httpBatchStreamLink',
});
}
const res: TRPCResponse<unknown, inferRouterError<TRouter>> = error
? {
error: getErrorShape({
config,
ctx: ctxManager.valueOrUndefined(),
error,
input: call!.result(),
path: call!.path,
type: info.type,
}),
}
: { result: { data: result.data } };
const headResponse = initResponse({
ctx: ctxManager.valueOrUndefined(),
info,
responseMeta: opts.responseMeta,
errors: error ? [error] : [],
headers,
untransformedJSON: [res],
});
return new Response(
JSON.stringify(transformTRPCResponse(config, res)),
{
status: headResponse.status,
headers,
},
);
}
case 'subscription': {
// httpSubscriptionLink
const iterable: AsyncIterable<unknown> = run(() => {
if (error) {
return errorToAsyncIterable(error);
}
if (!experimentalSSE) {
return errorToAsyncIterable(
new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: 'Missing experimental flag "sseSubscriptions"',
}),
);
}
if (!isObservable(result.data) && !isAsyncIterable(result.data)) {
return errorToAsyncIterable(
new TRPCError({
message: `Subscription ${
call!.path
} did not return an observable or a AsyncGenerator`,
code: 'INTERNAL_SERVER_ERROR',
}),
);
}
const dataAsIterable = isObservable(result.data)
? observableToAsyncIterable(result.data, opts.req.signal)
: result.data;
return dataAsIterable;
});
const stream = sseStreamProducer({
...config.sse,
data: iterable,
serialize: (v) => config.transformer.output.serialize(v),
formatError(errorOpts) {
const error = getTRPCErrorFromUnknown(errorOpts.error);
const input = call?.result();
const path = call?.path;
const type = call?.procedure?._def.type ?? 'unknown';
opts.onError?.({
error,
path,
input,
ctx: ctxManager.valueOrUndefined(),
req: opts.req,
type,
});
const shape = getErrorShape({
config,
ctx: ctxManager.valueOrUndefined(),
error,
input,
path,
type,
});
return shape;
},
});
for (const [key, value] of Object.entries(sseHeaders)) {
headers.set(key, value);
}
const headResponse = initResponse({
ctx: ctxManager.valueOrUndefined(),
info,
responseMeta: opts.responseMeta,
errors: [],
headers,
untransformedJSON: null,
});
const abortSignal = result?.signal;
let responseBody: ReadableStream<Uint8Array> = stream;
// Fixes: https://github.com/trpc/trpc/issues/7094
if (abortSignal) {
const reader = stream.getReader();
const onAbort = () => void reader.cancel();
if (abortSignal.aborted) {
onAbort();
} else {
abortSignal.addEventListener('abort', onAbort, { once: true });
}
responseBody = new ReadableStream({
async pull(controller) {
const chunk = await reader.read();
if (chunk.done) {
abortSignal.removeEventListener('abort', onAbort);
controller.close();
} else {
controller.enqueue(chunk.value);
}
},
cancel() {
abortSignal.removeEventListener('abort', onAbort);
return reader.cancel();
},
});
}
return new Response(responseBody, {
headers,
status: headResponse.status,
});
}
}
}
// batch response handlers
if (info.accept === 'application/jsonl') {
// httpBatchStreamLink
headers.set('content-type', 'application/json');
headers.set('transfer-encoding', 'chunked');
const headResponse = initResponse({
ctx: ctxManager.valueOrUndefined(),
info,
responseMeta: opts.responseMeta,
errors: [],
headers,
untransformedJSON: null,
});
const stream = jsonlStreamProducer({
...config.jsonl,
/**
* Example structure for `maxDepth: 4`:
* {
* // 1
* 0: {
* // 2
* result: {
* // 3
* data: // 4
* }
* }
* }
*/
maxDepth: Infinity,
data: rpcCalls.map(async (res, index) => {
const [error, result] = await res;
const call = info.calls[index];
if (error) {
return {
error: getErrorShape({
config,
ctx: ctxManager.valueOrUndefined(),
error,
input: call!.result(),
path: call!.path,
type: call!.procedure?._def.type ?? 'unknown',
}),
};
}
/**
* Not very pretty, but we need to wrap nested data in promises
* Our stream producer will only resolve top-level async values or async values that are directly nested in another async value
*/
const iterable = isObservable(result.data)
? observableToAsyncIterable(result.data, opts.req.signal)
: Promise.resolve(result.data);
return {
result: Promise.resolve({
data: iterable,
}),
};
}),
serialize: (data) => config.transformer.output.serialize(data),
onError: (cause) => {
opts.onError?.({
error: getTRPCErrorFromUnknown(cause),
path: undefined,
input: undefined,
ctx: ctxManager.valueOrUndefined(),
req: opts.req,
type: info?.type ?? 'unknown',
});
},
formatError(errorOpts) {
const call = info?.calls[errorOpts.path[0] as any];
const error = getTRPCErrorFromUnknown(errorOpts.error);
const input = call?.result();
const path = call?.path;
const type = call?.procedure?._def.type ?? 'unknown';
// no need to call `onError` here as it will be propagated through the stream itself
const shape = getErrorShape({
config,
ctx: ctxManager.valueOrUndefined(),
error,
input,
path,
type,
});
return shape;
},
});
return new Response(stream, {
headers,
status: headResponse.status,
});
}
// httpBatchLink
/**
* Non-streaming response:
* - await all responses in parallel, blocking on the slowest one
* - create headers with known response body
* - return a complete HTTPResponse
*/
headers.set('content-type', 'application/json');
const results: RPCResult[] = (await Promise.all(rpcCalls)).map(
(res): RPCResult => {
const [error, result] = res;
if (error) {
return res;
}
if (isDataStream(result.data)) {
return [
new TRPCError({
code: 'UNSUPPORTED_MEDIA_TYPE',
message:
'Cannot use stream-like response in non-streaming request - use httpBatchStreamLink',
}),
undefined,
];
}
return res;
},
);
const resultAsRPCResponse = results.map(
(
[error, result],
index,
): TRPCResponse<unknown, inferRouterError<TRouter>> => {
const call = info.calls[index]!;
if (error) {
return {
error: getErrorShape({
config,
ctx: ctxManager.valueOrUndefined(),
error,
input: call.result(),
path: call.path,
type: call.procedure?._def.type ?? 'unknown',
}),
};
}
return {
result: { data: result.data },
};
},
);
const errors = results
.map(([error]) => error)
.filter(Boolean) as TRPCError[];
const headResponse = initResponse({
ctx: ctxManager.valueOrUndefined(),
info,
responseMeta: opts.responseMeta,
untransformedJSON: resultAsRPCResponse,
errors,
headers,
});
return new Response(
JSON.stringify(transformTRPCResponse(config, resultAsRPCResponse)),
{
status: headResponse.status,
headers,
},
);
} catch (cause) {
const [_infoError, info] = infoTuple;
const ctx = ctxManager.valueOrUndefined();
// we get here if
// - batching is called when it's not enabled
// - `createContext()` throws
// - `router._def._config.transformer.output.serialize()` throws
// - post body is too large
// - input deserialization fails
// - `errorFormatter` return value is malformed
const { error, untransformedJSON, body } = caughtErrorToData(cause, {
opts,
ctx: ctxManager.valueOrUndefined(),
type: info?.type ?? 'unknown',
});
const headResponse = initResponse({
ctx,
info,
responseMeta: opts.responseMeta,
untransformedJSON,
errors: [error],
headers,
});
return new Response(body, {
status: headResponse.status,
headers,
});
}
}

View File

@@ -0,0 +1,168 @@
import type { TRPCError } from '../error/TRPCError';
import type {
AnyProcedure,
ErrorHandlerOptions,
ProcedureType,
} from '../procedure';
import type {
AnyRouter,
inferRouterContext,
inferRouterError,
} from '../router';
import type { TRPCResponse } from '../rpc';
import type { Dict } from '../types';
/**
* @deprecated use `Headers` instead, this will be removed in v12
*/
type HTTPHeaders = Dict<string[] | string>;
export interface ResponseMeta {
status?: number;
headers?: Headers | HTTPHeaders;
}
/**
* @internal
*/
export type ResponseMetaFn<TRouter extends AnyRouter> = (opts: {
data: TRPCResponse<unknown, inferRouterError<TRouter>>[];
ctx?: inferRouterContext<TRouter>;
/**
* The different tRPC paths requested
* @deprecated use `info` instead, this will be removed in v12
**/
paths: readonly string[] | undefined;
info: TRPCRequestInfo | undefined;
type: ProcedureType | 'unknown';
errors: TRPCError[];
/**
* `true` if the `ResponseMeta` is being generated without knowing the response data (e.g. for streamed requests).
*/
eagerGeneration: boolean;
}) => ResponseMeta;
/**
* Base interface for anything using HTTP
*/
export interface HTTPBaseHandlerOptions<TRouter extends AnyRouter, TRequest>
extends BaseHandlerOptions<TRouter, TRequest> {
/**
* Add handler to be called before response is sent to the user
* Useful for setting cache headers
* @see https://trpc.io/docs/v11/caching
*/
responseMeta?: ResponseMetaFn<TRouter>;
}
export type TRPCAcceptHeader = 'application/jsonl';
export interface TRPCRequestInfoProcedureCall {
path: string;
/**
* Read the raw input (deduped and memoized)
*/
getRawInput: () => Promise<unknown>;
/**
* Get already parsed inputs - won't trigger reading the body or parsing the inputs
*/
result: () => unknown;
/**
* The procedure being called, `null` if not found
* @internal
*/
procedure: AnyProcedure | null;
/**
* The index of this call in a batch request.
*/
batchIndex: number;
}
/**
* Information about the incoming request
* @public
*/
export interface TRPCRequestInfo {
/**
* The `trpc-accept` header
*/
accept: TRPCAcceptHeader | null;
/**
* The type of the request
*/
type: ProcedureType | 'unknown';
/**
* If the content type handler has detected that this is a batch call
*/
isBatchCall: boolean;
/**
* The calls being made
*/
calls: TRPCRequestInfoProcedureCall[];
/**
* Connection params when using `httpSubscriptionLink` or `createWSClient`
*/
connectionParams: Dict<string> | null;
/**
* Signal when the request is aborted
* Can be used to abort async operations during the request, e.g. `fetch()`-requests
*/
signal: AbortSignal;
/**
* The URL of the request if available
*/
url: URL | null;
}
/**
* Inner createContext function for `resolveResponse` used to forward `TRPCRequestInfo` to `createContext`
* @internal
*/
export type ResolveHTTPRequestOptionsContextFn<TRouter extends AnyRouter> =
(opts: { info: TRPCRequestInfo }) => Promise<inferRouterContext<TRouter>>;
interface HTTPErrorHandlerOptions<TRouter extends AnyRouter, TRequest>
extends ErrorHandlerOptions<inferRouterContext<TRouter>> {
req: TRequest;
}
/**
* @internal
*/
export type HTTPErrorHandler<TRouter extends AnyRouter, TRequest> = (
opts: HTTPErrorHandlerOptions<TRouter, TRequest>,
) => void;
/**
* Base interface for any response handler
* @internal
*/
export interface BaseHandlerOptions<TRouter extends AnyRouter, TRequest> {
onError?: HTTPErrorHandler<TRouter, TRequest>;
/**
* @deprecated use `allowBatching` instead, this will be removed in v12
*/
batching?: {
/**
* @default true
*/
enabled: boolean;
};
router: TRouter;
/**
* Allow method override - will skip the method check
* @default false
*/
allowMethodOverride?: boolean;
/**
* Allow request batching
* @default true
*/
allowBatching?: boolean;
/**
* Restrict the maximum size of a batch call, invalid requests will be rejected with a 400 error
*
* @important Ensure you set the same or lower limit on your client batch link
* @default unlimited
*/
maxBatchSize?: number;
}

View File

@@ -0,0 +1,222 @@
import {
defaultFormatter,
type DefaultErrorShape,
type ErrorFormatter,
} from './error/formatter';
import type { MiddlewareBuilder, MiddlewareFunction } from './middleware';
import { createMiddlewareFactory } from './middleware';
import type { ProcedureBuilder } from './procedureBuilder';
import { createBuilder } from './procedureBuilder';
import type { AnyRootTypes, CreateRootTypes } from './rootConfig';
import { isServerDefault, type RootConfig } from './rootConfig';
import type {
AnyRouter,
MergeRouters,
RouterBuilder,
RouterCallerFactory,
} from './router';
import {
createCallerFactory,
createRouterFactory,
mergeRouters,
} from './router';
import type { DataTransformerOptions } from './transformer';
import { defaultTransformer, getDataTransformer } from './transformer';
import type { Unwrap, ValidateShape } from './types';
import type { UnsetMarker } from './utils';
type inferErrorFormatterShape<TType> =
TType extends ErrorFormatter<any, infer TShape> ? TShape : DefaultErrorShape;
/** @internal */
export interface RuntimeConfigOptions<
TContext extends object,
TMeta extends object,
> extends Partial<
Omit<
RootConfig<{
ctx: TContext;
meta: TMeta;
errorShape: any;
transformer: any;
}>,
'$types' | 'transformer'
>
> {
/**
* Use a data transformer
* @see https://trpc.io/docs/v11/data-transformers
*/
transformer?: DataTransformerOptions;
}
type ContextCallback = (...args: any[]) => object | Promise<object>;
export interface TRPCRootObject<
TContext extends object,
TMeta extends object,
TOptions extends RuntimeConfigOptions<TContext, TMeta>,
$Root extends AnyRootTypes = {
ctx: TContext;
meta: TMeta;
errorShape: undefined extends TOptions['errorFormatter']
? DefaultErrorShape
: inferErrorFormatterShape<TOptions['errorFormatter']>;
transformer: undefined extends TOptions['transformer'] ? false : true;
},
> {
/**
* Your router config
* @internal
*/
_config: RootConfig<$Root>;
/**
* Builder object for creating procedures
* @see https://trpc.io/docs/v11/server/procedures
*/
procedure: ProcedureBuilder<
TContext,
TMeta,
object,
UnsetMarker,
UnsetMarker,
UnsetMarker,
UnsetMarker,
false
>;
/**
* Create reusable middlewares
* @see https://trpc.io/docs/v11/server/middlewares
*/
middleware: <$ContextOverrides>(
fn: MiddlewareFunction<TContext, TMeta, object, $ContextOverrides, unknown>,
) => MiddlewareBuilder<TContext, TMeta, $ContextOverrides, unknown>;
/**
* Create a router
* @see https://trpc.io/docs/v11/server/routers
*/
router: RouterBuilder<$Root>;
/**
* Merge Routers
* @see https://trpc.io/docs/v11/server/merging-routers
*/
mergeRouters: <TRouters extends AnyRouter[]>(
...routerList: [...TRouters]
) => MergeRouters<TRouters>;
/**
* Create a server-side caller for a router
* @see https://trpc.io/docs/v11/server/server-side-calls
*/
createCallerFactory: RouterCallerFactory<$Root>;
}
class TRPCBuilder<TContext extends object, TMeta extends object> {
/**
* Add a context shape as a generic to the root object
* @see https://trpc.io/docs/v11/server/context
*/
context<TNewContext extends object | ContextCallback>() {
return new TRPCBuilder<
TNewContext extends ContextCallback ? Unwrap<TNewContext> : TNewContext,
TMeta
>();
}
/**
* Add a meta shape as a generic to the root object
* @see https://trpc.io/docs/v11/quickstart
*/
meta<TNewMeta extends object>() {
return new TRPCBuilder<TContext, TNewMeta>();
}
/**
* Create the root object
* @see https://trpc.io/docs/v11/server/routers#initialize-trpc
*/
create<TOptions extends RuntimeConfigOptions<TContext, TMeta>>(
opts?: ValidateShape<TOptions, RuntimeConfigOptions<TContext, TMeta>>,
): TRPCRootObject<TContext, TMeta, TOptions> {
type $Root = CreateRootTypes<{
ctx: TContext;
meta: TMeta;
errorShape: undefined extends TOptions['errorFormatter']
? DefaultErrorShape
: inferErrorFormatterShape<TOptions['errorFormatter']>;
transformer: undefined extends TOptions['transformer'] ? false : true;
}>;
const config: RootConfig<$Root> = {
...opts,
transformer: getDataTransformer(opts?.transformer ?? defaultTransformer),
isDev:
opts?.isDev ??
// eslint-disable-next-line @typescript-eslint/dot-notation
globalThis.process?.env['NODE_ENV'] !== 'production',
allowOutsideOfServer: opts?.allowOutsideOfServer ?? false,
errorFormatter: opts?.errorFormatter ?? defaultFormatter,
isServer: opts?.isServer ?? isServerDefault,
/**
* These are just types, they can't be used at runtime
* @internal
*/
$types: null as any,
};
{
// Server check
const isServer: boolean = opts?.isServer ?? isServerDefault;
if (!isServer && opts?.allowOutsideOfServer !== true) {
throw new Error(
`You're trying to use @trpc/server in a non-server environment. This is not supported by default.`,
);
}
}
return {
/**
* Your router config
* @internal
*/
_config: config,
/**
* Builder object for creating procedures
* @see https://trpc.io/docs/v11/server/procedures
*/
procedure: createBuilder<$Root['ctx'], $Root['meta']>({
meta: opts?.defaultMeta,
}),
/**
* Create reusable middlewares
* @see https://trpc.io/docs/v11/server/middlewares
*/
middleware: createMiddlewareFactory<$Root['ctx'], $Root['meta']>(),
/**
* Create a router
* @see https://trpc.io/docs/v11/server/routers
*/
router: createRouterFactory<$Root>(config),
/**
* Merge Routers
* @see https://trpc.io/docs/v11/server/merging-routers
*/
mergeRouters,
/**
* Create a server-side caller for a router
* @see https://trpc.io/docs/v11/server/server-side-calls
*/
createCallerFactory: createCallerFactory<$Root>(),
};
}
}
/**
* Builder to initialize the tRPC root object - use this exactly once per backend
* @see https://trpc.io/docs/v11/quickstart
*/
export const initTRPC = new TRPCBuilder();
export type { TRPCBuilder };

View File

@@ -0,0 +1,243 @@
import { TRPCError } from './error/TRPCError';
import type { ParseFn } from './parser';
import type { ProcedureType } from './procedure';
import type { GetRawInputFn, Overwrite, Simplify } from './types';
import { isObject } from './utils';
/** @internal */
export const middlewareMarker = 'middlewareMarker' as 'middlewareMarker' & {
__brand: 'middlewareMarker';
};
type MiddlewareMarker = typeof middlewareMarker;
interface MiddlewareResultBase {
/**
* All middlewares should pass through their `next()`'s output.
* Requiring this marker makes sure that can't be forgotten at compile-time.
*/
readonly marker: MiddlewareMarker;
}
interface MiddlewareOKResult<_TContextOverride> extends MiddlewareResultBase {
ok: true;
data: unknown;
// this could be extended with `input`/`rawInput` later
}
interface MiddlewareErrorResult<_TContextOverride>
extends MiddlewareResultBase {
ok: false;
error: TRPCError;
}
/**
* @internal
*/
export type MiddlewareResult<_TContextOverride> =
| MiddlewareErrorResult<_TContextOverride>
| MiddlewareOKResult<_TContextOverride>;
/**
* @internal
*/
export interface MiddlewareBuilder<
TContext,
TMeta,
TContextOverrides,
TInputOut,
> {
/**
* Create a new builder based on the current middleware builder
*/
unstable_pipe<$ContextOverridesOut>(
fn:
| MiddlewareFunction<
TContext,
TMeta,
TContextOverrides,
$ContextOverridesOut,
TInputOut
>
| MiddlewareBuilder<
Overwrite<TContext, TContextOverrides>,
TMeta,
$ContextOverridesOut,
TInputOut
>,
): MiddlewareBuilder<
TContext,
TMeta,
Overwrite<TContextOverrides, $ContextOverridesOut>,
TInputOut
>;
/**
* List of middlewares within this middleware builder
*/
_middlewares: MiddlewareFunction<
TContext,
TMeta,
TContextOverrides,
object,
TInputOut
>[];
}
/**
* @internal
*/
export type MiddlewareFunction<
TContext,
TMeta,
TContextOverridesIn,
$ContextOverridesOut,
TInputOut,
> = {
(opts: {
ctx: Simplify<Overwrite<TContext, TContextOverridesIn>>;
type: ProcedureType;
path: string;
input: TInputOut;
getRawInput: GetRawInputFn;
meta: TMeta | undefined;
signal: AbortSignal | undefined;
/**
* The index of this call in a batch request.
*/
batchIndex: number;
next: {
(): Promise<MiddlewareResult<TContextOverridesIn>>;
<$ContextOverride>(opts: {
ctx?: $ContextOverride;
input?: unknown;
}): Promise<MiddlewareResult<$ContextOverride>>;
(opts: {
getRawInput: GetRawInputFn;
}): Promise<MiddlewareResult<TContextOverridesIn>>;
};
}): Promise<MiddlewareResult<$ContextOverridesOut>>;
_type?: string | undefined;
};
export type AnyMiddlewareFunction = MiddlewareFunction<any, any, any, any, any>;
export type AnyMiddlewareBuilder = MiddlewareBuilder<any, any, any, any>;
/**
* @internal
*/
export function createMiddlewareFactory<
TContext,
TMeta,
TInputOut = unknown,
>() {
function createMiddlewareInner(
middlewares: AnyMiddlewareFunction[],
): AnyMiddlewareBuilder {
return {
_middlewares: middlewares,
unstable_pipe(middlewareBuilderOrFn) {
const pipedMiddleware =
'_middlewares' in middlewareBuilderOrFn
? middlewareBuilderOrFn._middlewares
: [middlewareBuilderOrFn];
return createMiddlewareInner([...middlewares, ...pipedMiddleware]);
},
};
}
function createMiddleware<$ContextOverrides>(
fn: MiddlewareFunction<
TContext,
TMeta,
object,
$ContextOverrides,
TInputOut
>,
): MiddlewareBuilder<TContext, TMeta, $ContextOverrides, TInputOut> {
return createMiddlewareInner([fn]);
}
return createMiddleware;
}
/**
* Create a standalone middleware
* @see https://trpc.io/docs/v11/server/middlewares#experimental-standalone-middlewares
* @deprecated use `.concat()` instead
*/
export const experimental_standaloneMiddleware = <
TCtx extends {
ctx?: object;
meta?: object;
input?: unknown;
},
>() => ({
create: createMiddlewareFactory<
TCtx extends { ctx: infer T extends object } ? T : any,
TCtx extends { meta: infer T extends object } ? T : object,
TCtx extends { input: infer T } ? T : unknown
>(),
});
/**
* @internal
* Please note, `trpc-openapi` uses this function.
*/
export function createInputMiddleware<TInput>(parse: ParseFn<TInput>) {
const inputMiddleware: AnyMiddlewareFunction =
async function inputValidatorMiddleware(opts) {
let parsedInput: ReturnType<typeof parse>;
const rawInput = await opts.getRawInput();
try {
parsedInput = await parse(rawInput);
} catch (cause) {
throw new TRPCError({
code: 'BAD_REQUEST',
cause,
});
}
// Multiple input parsers
const combinedInput =
isObject(opts.input) && isObject(parsedInput)
? {
...opts.input,
...parsedInput,
}
: parsedInput;
return opts.next({ input: combinedInput });
};
inputMiddleware._type = 'input';
return inputMiddleware;
}
/**
* @internal
*/
export function createOutputMiddleware<TOutput>(parse: ParseFn<TOutput>) {
const outputMiddleware: AnyMiddlewareFunction =
async function outputValidatorMiddleware({ next }) {
const result = await next();
if (!result.ok) {
// pass through failures without validating
return result;
}
try {
const data = await parse(result.data);
return {
...result,
data,
};
} catch (cause) {
throw new TRPCError({
message: 'Output validation failed',
code: 'INTERNAL_SERVER_ERROR',
cause,
});
}
};
outputMiddleware._type = 'output';
return outputMiddleware;
}

View File

@@ -0,0 +1,140 @@
import { StandardSchemaV1Error } from '../vendor/standard-schema-v1/error';
import { type StandardSchemaV1 } from '../vendor/standard-schema-v1/spec';
// zod / typeschema
export type ParserZodEsque<TInput, TParsedInput> = {
_input: TInput;
_output: TParsedInput;
};
export type ParserValibotEsque<TInput, TParsedInput> = {
schema: {
_types?: {
input: TInput;
output: TParsedInput;
};
};
};
export type ParserArkTypeEsque<TInput, TParsedInput> = {
inferIn: TInput;
infer: TParsedInput;
};
export type ParserStandardSchemaEsque<TInput, TParsedInput> = StandardSchemaV1<
TInput,
TParsedInput
>;
export type ParserMyZodEsque<TInput> = {
parse: (input: any) => TInput;
};
export type ParserSuperstructEsque<TInput> = {
create: (input: unknown) => TInput;
};
export type ParserCustomValidatorEsque<TInput> = (
input: unknown,
) => Promise<TInput> | TInput;
export type ParserYupEsque<TInput> = {
validateSync: (input: unknown) => TInput;
};
export type ParserScaleEsque<TInput> = {
assert(value: unknown): asserts value is TInput;
};
export type ParserWithoutInput<TInput> =
| ParserCustomValidatorEsque<TInput>
| ParserMyZodEsque<TInput>
| ParserScaleEsque<TInput>
| ParserSuperstructEsque<TInput>
| ParserYupEsque<TInput>;
export type ParserWithInputOutput<TInput, TParsedInput> =
| ParserZodEsque<TInput, TParsedInput>
| ParserValibotEsque<TInput, TParsedInput>
| ParserArkTypeEsque<TInput, TParsedInput>
| ParserStandardSchemaEsque<TInput, TParsedInput>;
export type Parser = ParserWithInputOutput<any, any> | ParserWithoutInput<any>;
export type inferParser<TParser extends Parser> =
TParser extends ParserStandardSchemaEsque<infer $TIn, infer $TOut>
? {
in: $TIn;
out: $TOut;
}
: TParser extends ParserWithInputOutput<infer $TIn, infer $TOut>
? {
in: $TIn;
out: $TOut;
}
: TParser extends ParserWithoutInput<infer $InOut>
? {
in: $InOut;
out: $InOut;
}
: never;
export type ParseFn<TType> = (value: unknown) => Promise<TType> | TType;
export function getParseFn<TType>(procedureParser: Parser): ParseFn<TType> {
const parser = procedureParser as any;
const isStandardSchema = '~standard' in parser;
if (typeof parser === 'function' && typeof parser.assert === 'function') {
// ParserArkTypeEsque - arktype schemas shouldn't be called as a function because they return a union type instead of throwing
return parser.assert.bind(parser);
}
if (typeof parser === 'function' && !isStandardSchema) {
// ParserValibotEsque (>= v0.31.0)
// ParserCustomValidatorEsque - note the check for standard-schema conformance - some libraries like `effect` use function schemas which are *not* a "parse" function.
return parser;
}
if (typeof parser.parseAsync === 'function') {
// ParserZodEsque
return parser.parseAsync.bind(parser);
}
if (typeof parser.parse === 'function') {
// ParserZodEsque
// ParserValibotEsque (< v0.13.0)
return parser.parse.bind(parser);
}
if (typeof parser.validateSync === 'function') {
// ParserYupEsque
return parser.validateSync.bind(parser);
}
if (typeof parser.create === 'function') {
// ParserSuperstructEsque
return parser.create.bind(parser);
}
if (typeof parser.assert === 'function') {
// ParserScaleEsque
return (value) => {
parser.assert(value);
return value as TType;
};
}
if (isStandardSchema) {
// StandardSchemaEsque
return async (value) => {
const result = await parser['~standard'].validate(value);
if (result.issues) {
throw new StandardSchemaV1Error(result.issues);
}
return result.value;
};
}
throw new Error('Could not find a validator fn');
}

View File

@@ -0,0 +1,103 @@
import type { TRPCError } from './error/TRPCError';
import type { Parser } from './parser';
import type { ProcedureCallOptions } from './procedureBuilder';
export const procedureTypes = ['query', 'mutation', 'subscription'] as const;
/**
* @public
*/
export type ProcedureType = (typeof procedureTypes)[number];
interface BuiltProcedureDef {
meta: unknown;
input: unknown;
output: unknown;
}
/**
*
* @internal
*/
export interface Procedure<
TType extends ProcedureType,
TDef extends BuiltProcedureDef,
> {
_def: {
/**
* These are just types, they can't be used at runtime
* @internal
*/
$types: {
input: TDef['input'];
output: TDef['output'];
};
procedure: true;
type: TType;
/**
* @internal
* Meta is not inferrable on individual procedures, only on the router
*/
meta: unknown;
experimental_caller: boolean;
/**
* The input parsers for the procedure
*/
inputs: Parser[];
};
meta: TDef['meta'];
/**
* @internal
*/
(opts: ProcedureCallOptions<unknown>): Promise<TDef['output']>;
}
export interface QueryProcedure<TDef extends BuiltProcedureDef>
extends Procedure<'query', TDef> {}
export interface MutationProcedure<TDef extends BuiltProcedureDef>
extends Procedure<'mutation', TDef> {}
export interface SubscriptionProcedure<TDef extends BuiltProcedureDef>
extends Procedure<'subscription', TDef> {}
/**
* @deprecated
*/
export interface LegacyObservableSubscriptionProcedure<
TDef extends BuiltProcedureDef,
> extends SubscriptionProcedure<TDef> {
_observable: true;
}
export type AnyQueryProcedure = QueryProcedure<any>;
export type AnyMutationProcedure = MutationProcedure<any>;
export type AnySubscriptionProcedure =
| SubscriptionProcedure<any>
| LegacyObservableSubscriptionProcedure<any>;
export type AnyProcedure =
| AnyQueryProcedure
| AnyMutationProcedure
| AnySubscriptionProcedure;
export type inferProcedureInput<TProcedure extends AnyProcedure> =
undefined extends inferProcedureParams<TProcedure>['$types']['input']
? void | inferProcedureParams<TProcedure>['$types']['input']
: inferProcedureParams<TProcedure>['$types']['input'];
export type inferProcedureParams<TProcedure> = TProcedure extends AnyProcedure
? TProcedure['_def']
: never;
export type inferProcedureOutput<TProcedure> =
inferProcedureParams<TProcedure>['$types']['output'];
/**
* @internal
*/
export interface ErrorHandlerOptions<TContext> {
error: TRPCError;
type: ProcedureType | 'unknown';
path: string | undefined;
input: unknown;
ctx: TContext | undefined;
}

View File

@@ -0,0 +1,704 @@
import type { inferObservableValue, Observable } from '../observable';
import { getTRPCErrorFromUnknown, TRPCError } from './error/TRPCError';
import type {
AnyMiddlewareFunction,
MiddlewareBuilder,
MiddlewareFunction,
MiddlewareResult,
} from './middleware';
import {
createInputMiddleware,
createOutputMiddleware,
middlewareMarker,
} from './middleware';
import type { inferParser, Parser } from './parser';
import { getParseFn } from './parser';
import type {
AnyMutationProcedure,
AnyProcedure,
AnyQueryProcedure,
LegacyObservableSubscriptionProcedure,
MutationProcedure,
ProcedureType,
QueryProcedure,
SubscriptionProcedure,
} from './procedure';
import type { inferTrackedOutput } from './stream/tracked';
import type {
GetRawInputFn,
MaybePromise,
Overwrite,
Simplify,
TypeError,
} from './types';
import type { UnsetMarker } from './utils';
import { mergeWithoutOverrides } from './utils';
type IntersectIfDefined<TType, TWith> = TType extends UnsetMarker
? TWith
: TWith extends UnsetMarker
? TType
: Simplify<TType & TWith>;
type DefaultValue<TValue, TFallback> = TValue extends UnsetMarker
? TFallback
: TValue;
type inferAsyncIterable<TOutput> =
TOutput extends AsyncIterable<infer $Yield, infer $Return, infer $Next>
? {
yield: $Yield;
return: $Return;
next: $Next;
}
: never;
type inferSubscriptionOutput<TOutput> =
TOutput extends AsyncIterable<any>
? AsyncIterable<
inferTrackedOutput<inferAsyncIterable<TOutput>['yield']>,
inferAsyncIterable<TOutput>['return'],
inferAsyncIterable<TOutput>['next']
>
: TypeError<'Subscription output could not be inferred'>;
export type CallerOverride<TContext> = (opts: {
args: unknown[];
invoke: (opts: ProcedureCallOptions<TContext>) => Promise<unknown>;
_def: AnyProcedure['_def'];
}) => Promise<unknown>;
type ProcedureBuilderDef<TMeta> = {
procedure: true;
inputs: Parser[];
output?: Parser;
meta?: TMeta;
resolver?: ProcedureBuilderResolver;
middlewares: AnyMiddlewareFunction[];
/**
* @deprecated use `type` instead
*/
mutation?: boolean;
/**
* @deprecated use `type` instead
*/
query?: boolean;
/**
* @deprecated use `type` instead
*/
subscription?: boolean;
type?: ProcedureType;
caller?: CallerOverride<unknown>;
};
type AnyProcedureBuilderDef = ProcedureBuilderDef<any>;
/**
* Procedure resolver options (what the `.query()`, `.mutation()`, and `.subscription()` functions receive)
* @internal
*/
export interface ProcedureResolverOptions<
TContext,
_TMeta,
TContextOverridesIn,
TInputOut,
> {
ctx: Simplify<Overwrite<TContext, TContextOverridesIn>>;
input: TInputOut extends UnsetMarker ? undefined : TInputOut;
/**
* The AbortSignal of the request
*/
signal: AbortSignal | undefined;
/**
* The path of the procedure
*/
path: string;
/**
* The index of this call in a batch request.
* Will be set when the procedure is called as part of a batch.
*/
batchIndex?: number;
}
/**
* A procedure resolver
*/
type ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputParserIn,
$Output,
> = (
opts: ProcedureResolverOptions<TContext, TMeta, TContextOverrides, TInputOut>,
) => MaybePromise<
// If an output parser is defined, we need to return what the parser expects, otherwise we return the inferred type
DefaultValue<TOutputParserIn, $Output>
>;
type AnyResolver = ProcedureResolver<any, any, any, any, any, any>;
export type AnyProcedureBuilder = ProcedureBuilder<
any,
any,
any,
any,
any,
any,
any,
any
>;
/**
* Infer the context type from a procedure builder
* Useful to create common helper functions for different procedures
*/
export type inferProcedureBuilderResolverOptions<
TProcedureBuilder extends AnyProcedureBuilder,
> =
TProcedureBuilder extends ProcedureBuilder<
infer TContext,
infer TMeta,
infer TContextOverrides,
infer _TInputIn,
infer TInputOut,
infer _TOutputIn,
infer _TOutputOut,
infer _TCaller
>
? ProcedureResolverOptions<
TContext,
TMeta,
TContextOverrides,
TInputOut extends UnsetMarker
? // if input is not set, we don't want to infer it as `undefined` since a procedure further down the chain might have set an input
unknown
: TInputOut extends object
? Simplify<
TInputOut & {
/**
* Extra input params might have been added by a `.input()` further down the chain
*/
[keyAddedByInputCallFurtherDown: string]: unknown;
}
>
: TInputOut
>
: never;
export interface ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller extends boolean,
> {
/**
* Add an input parser to the procedure.
* @see https://trpc.io/docs/v11/server/validators
*/
input<$Parser extends Parser>(
schema: TInputOut extends UnsetMarker
? $Parser
: inferParser<$Parser>['out'] extends Record<string, unknown> | undefined
? TInputOut extends Record<string, unknown> | undefined
? undefined extends inferParser<$Parser>['out'] // if current is optional the previous must be too
? undefined extends TInputOut
? $Parser
: TypeError<'Cannot chain an optional parser to a required parser'>
: $Parser
: TypeError<'All input parsers did not resolve to an object'>
: TypeError<'All input parsers did not resolve to an object'>,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
IntersectIfDefined<TInputIn, inferParser<$Parser>['in']>,
IntersectIfDefined<TInputOut, inferParser<$Parser>['out']>,
TOutputIn,
TOutputOut,
TCaller
>;
/**
* Add an output parser to the procedure.
* @see https://trpc.io/docs/v11/server/validators
*/
output<$Parser extends Parser>(
schema: $Parser,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
IntersectIfDefined<TOutputIn, inferParser<$Parser>['in']>,
IntersectIfDefined<TOutputOut, inferParser<$Parser>['out']>,
TCaller
>;
/**
* Add a meta data to the procedure.
* @see https://trpc.io/docs/v11/server/metadata
*/
meta(
meta: TMeta,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller
>;
/**
* Add a middleware to the procedure.
* @see https://trpc.io/docs/v11/server/middlewares
*/
use<$ContextOverridesOut>(
fn:
| MiddlewareBuilder<
Overwrite<TContext, TContextOverrides>,
TMeta,
$ContextOverridesOut,
TInputOut
>
| MiddlewareFunction<
TContext,
TMeta,
TContextOverrides,
$ContextOverridesOut,
TInputOut
>,
): ProcedureBuilder<
TContext,
TMeta,
Overwrite<TContextOverrides, $ContextOverridesOut>,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller
>;
/**
* @deprecated use {@link concat} instead
*/
unstable_concat<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
>(
builder: Overwrite<TContext, TContextOverrides> extends $Context
? TMeta extends $Meta
? ProcedureBuilder<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
TCaller
>
: TypeError<'Meta mismatch'>
: TypeError<'Context mismatch'>,
): ProcedureBuilder<
TContext,
TMeta,
Overwrite<TContextOverrides, $ContextOverrides>,
IntersectIfDefined<TInputIn, $InputIn>,
IntersectIfDefined<TInputOut, $InputOut>,
IntersectIfDefined<TOutputIn, $OutputIn>,
IntersectIfDefined<TOutputOut, $OutputOut>,
TCaller
>;
/**
* Combine two procedure builders
*/
concat<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
>(
builder: Overwrite<TContext, TContextOverrides> extends $Context
? TMeta extends $Meta
? ProcedureBuilder<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
TCaller
>
: TypeError<'Meta mismatch'>
: TypeError<'Context mismatch'>,
): ProcedureBuilder<
TContext,
TMeta,
Overwrite<TContextOverrides, $ContextOverrides>,
IntersectIfDefined<TInputIn, $InputIn>,
IntersectIfDefined<TInputOut, $InputOut>,
IntersectIfDefined<TOutputIn, $OutputIn>,
IntersectIfDefined<TOutputOut, $OutputOut>,
TCaller
>;
/**
* Query procedure
* @see https://trpc.io/docs/v11/concepts#vocabulary
*/
query<$Output>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? (
input: DefaultValue<TInputIn, void>,
) => Promise<DefaultValue<TOutputOut, $Output>>
: QueryProcedure<{
input: DefaultValue<TInputIn, void>;
output: DefaultValue<TOutputOut, $Output>;
meta: TMeta;
}>;
/**
* Mutation procedure
* @see https://trpc.io/docs/v11/concepts#vocabulary
*/
mutation<$Output>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? (
input: DefaultValue<TInputIn, void>,
) => Promise<DefaultValue<TOutputOut, $Output>>
: MutationProcedure<{
input: DefaultValue<TInputIn, void>;
output: DefaultValue<TOutputOut, $Output>;
meta: TMeta;
}>;
/**
* Subscription procedure
* @see https://trpc.io/docs/v11/server/subscriptions
*/
subscription<$Output extends AsyncIterable<any, void, any>>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? TypeError<'Not implemented'>
: SubscriptionProcedure<{
input: DefaultValue<TInputIn, void>;
output: inferSubscriptionOutput<DefaultValue<TOutputOut, $Output>>;
meta: TMeta;
}>;
/**
* @deprecated Using subscriptions with an observable is deprecated. Use an async generator instead.
* This feature will be removed in v12 of tRPC.
* @see https://trpc.io/docs/v11/server/subscriptions
*/
subscription<$Output extends Observable<any, any>>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? TypeError<'Not implemented'>
: LegacyObservableSubscriptionProcedure<{
input: DefaultValue<TInputIn, void>;
output: inferObservableValue<DefaultValue<TOutputOut, $Output>>;
meta: TMeta;
}>;
/**
* Overrides the way a procedure is invoked
* Do not use this unless you know what you're doing - this is an experimental API
*/
experimental_caller(
caller: CallerOverride<TContext>,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
true
>;
/**
* @internal
*/
_def: ProcedureBuilderDef<TMeta>;
}
type ProcedureBuilderResolver = (
opts: ProcedureResolverOptions<any, any, any, any>,
) => Promise<unknown>;
function createNewBuilder(
def1: AnyProcedureBuilderDef,
def2: Partial<AnyProcedureBuilderDef>,
): AnyProcedureBuilder {
const { middlewares = [], inputs, meta, ...rest } = def2;
// TODO: maybe have a fn here to warn about calls
return createBuilder({
...mergeWithoutOverrides(def1, rest),
inputs: [...def1.inputs, ...(inputs ?? [])],
middlewares: [...def1.middlewares, ...middlewares],
meta: def1.meta && meta ? { ...def1.meta, ...meta } : (meta ?? def1.meta),
});
}
export function createBuilder<TContext, TMeta>(
initDef: Partial<AnyProcedureBuilderDef> = {},
): ProcedureBuilder<
TContext,
TMeta,
object,
UnsetMarker,
UnsetMarker,
UnsetMarker,
UnsetMarker,
false
> {
const _def: AnyProcedureBuilderDef = {
procedure: true,
inputs: [],
middlewares: [],
...initDef,
};
const builder: AnyProcedureBuilder = {
_def,
input(input) {
const parser = getParseFn(input as Parser);
return createNewBuilder(_def, {
inputs: [input as Parser],
middlewares: [createInputMiddleware(parser)],
});
},
output(output: Parser) {
const parser = getParseFn(output);
return createNewBuilder(_def, {
output,
middlewares: [createOutputMiddleware(parser)],
});
},
meta(meta) {
return createNewBuilder(_def, {
meta,
});
},
use(middlewareBuilderOrFn) {
// Distinguish between a middleware builder and a middleware function
const middlewares =
'_middlewares' in middlewareBuilderOrFn
? middlewareBuilderOrFn._middlewares
: [middlewareBuilderOrFn];
return createNewBuilder(_def, {
middlewares: middlewares,
});
},
unstable_concat(builder) {
return createNewBuilder(_def, (builder as AnyProcedureBuilder)._def);
},
concat(builder) {
return createNewBuilder(_def, (builder as AnyProcedureBuilder)._def);
},
query(resolver) {
return createResolver(
{ ..._def, type: 'query' },
resolver,
) as AnyQueryProcedure;
},
mutation(resolver) {
return createResolver(
{ ..._def, type: 'mutation' },
resolver,
) as AnyMutationProcedure;
},
subscription(resolver: ProcedureResolver<any, any, any, any, any, any>) {
return createResolver({ ..._def, type: 'subscription' }, resolver) as any;
},
experimental_caller(caller) {
return createNewBuilder(_def, {
caller,
}) as any;
},
};
return builder;
}
function createResolver(
_defIn: AnyProcedureBuilderDef & { type: ProcedureType },
resolver: AnyResolver,
) {
const finalBuilder = createNewBuilder(_defIn, {
resolver,
middlewares: [
async function resolveMiddleware(opts) {
const data = await resolver(opts);
return {
marker: middlewareMarker,
ok: true,
data,
ctx: opts.ctx,
} as const;
},
],
});
const _def: AnyProcedure['_def'] = {
...finalBuilder._def,
type: _defIn.type,
experimental_caller: Boolean(finalBuilder._def.caller),
meta: finalBuilder._def.meta,
$types: null as any,
};
const invoke = createProcedureCaller(finalBuilder._def);
const callerOverride = finalBuilder._def.caller;
if (!callerOverride) {
return invoke;
}
const callerWrapper = async (...args: unknown[]) => {
return await callerOverride({
args,
invoke,
_def: _def,
});
};
callerWrapper._def = _def;
return callerWrapper;
}
/**
* @internal
*/
export interface ProcedureCallOptions<TContext> {
ctx: TContext;
getRawInput: GetRawInputFn;
input?: unknown;
path: string;
type: ProcedureType;
signal: AbortSignal | undefined;
/**
* The index of this call in a batch request.
*/
batchIndex: number;
}
const codeblock = `
This is a client-only function.
If you want to call this function on the server, see https://trpc.io/docs/v11/server/server-side-calls
`.trim();
// run the middlewares recursively with the resolver as the last one
async function callRecursive(
index: number,
_def: AnyProcedureBuilderDef,
opts: ProcedureCallOptions<any>,
): Promise<MiddlewareResult<any>> {
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const middleware = _def.middlewares[index]!;
const result = await middleware({
...opts,
meta: _def.meta,
input: opts.input,
next(_nextOpts?: any) {
const nextOpts = _nextOpts as
| {
ctx?: Record<string, unknown>;
input?: unknown;
getRawInput?: GetRawInputFn;
}
| undefined;
return callRecursive(index + 1, _def, {
...opts,
ctx: nextOpts?.ctx ? { ...opts.ctx, ...nextOpts.ctx } : opts.ctx,
input: nextOpts && 'input' in nextOpts ? nextOpts.input : opts.input,
getRawInput: nextOpts?.getRawInput ?? opts.getRawInput,
});
},
});
return result;
} catch (cause) {
return {
ok: false,
error: getTRPCErrorFromUnknown(cause),
marker: middlewareMarker,
};
}
}
function createProcedureCaller(_def: AnyProcedureBuilderDef): AnyProcedure {
async function procedure(opts: ProcedureCallOptions<unknown>) {
// is direct server-side call
if (!opts || !('getRawInput' in opts)) {
throw new Error(codeblock);
}
// there's always at least one "next" since we wrap this.resolver in a middleware
const result = await callRecursive(0, _def, opts);
if (!result) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message:
'No result from middlewares - did you forget to `return next()`?',
});
}
if (!result.ok) {
// re-throw original error
throw result.error;
}
return result.data;
}
procedure._def = _def;
procedure.procedure = true;
procedure.meta = _def.meta;
// FIXME typecast shouldn't be needed - fixittt
return procedure as unknown as AnyProcedure;
}

View File

@@ -0,0 +1,123 @@
import type { CombinedDataTransformer } from '../unstable-core-do-not-import';
import type { DefaultErrorShape, ErrorFormatter } from './error/formatter';
import type { JSONLProducerOptions } from './stream/jsonl';
import type { SSEStreamProducerOptions } from './stream/sse';
/**
* The initial generics that are used in the init function
* @internal
*/
export interface RootTypes {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: boolean;
}
/**
* The default check to see if we're in a server
*/
export const isServerDefault: boolean =
typeof window === 'undefined' ||
'Deno' in window ||
// eslint-disable-next-line @typescript-eslint/dot-notation
globalThis.process?.env?.['NODE_ENV'] === 'test' ||
!!globalThis.process?.env?.['JEST_WORKER_ID'] ||
!!globalThis.process?.env?.['VITEST_WORKER_ID'];
/**
* The tRPC root config
* @internal
*/
export interface RootConfig<TTypes extends RootTypes> {
/**
* The types that are used in the config
* @internal
*/
$types: TTypes;
/**
* Use a data transformer
* @see https://trpc.io/docs/v11/data-transformers
*/
transformer: CombinedDataTransformer;
/**
* Use custom error formatting
* @see https://trpc.io/docs/v11/error-formatting
*/
errorFormatter: ErrorFormatter<TTypes['ctx'], TTypes['errorShape']>;
/**
* Allow `@trpc/server` to run in non-server environments
* @warning **Use with caution**, this should likely mainly be used within testing.
* @default false
*/
allowOutsideOfServer: boolean;
/**
* Is this a server environment?
* @warning **Use with caution**, this should likely mainly be used within testing.
* @default typeof window === 'undefined' || 'Deno' in window || process.env.NODE_ENV === 'test'
*/
isServer: boolean;
/**
* Is this development?
* Will be used to decide if the API should return stack traces
* @default process.env.NODE_ENV !== 'production'
*/
isDev: boolean;
defaultMeta?: TTypes['meta'] extends object ? TTypes['meta'] : never;
/**
* Options for server-sent events (SSE) subscriptions
* @see https://trpc.io/docs/client/links/httpSubscriptionLink
*/
sse?: {
/**
* Enable server-sent events (SSE) subscriptions
* @default true
*/
enabled?: boolean;
} & Pick<
SSEStreamProducerOptions,
'ping' | 'emitAndEndImmediately' | 'maxDurationMs' | 'client'
>;
/**
* Options for batch stream
* @see https://trpc.io/docs/client/links/httpBatchStreamLink
*/
jsonl?: Pick<JSONLProducerOptions, 'pingMs'>;
experimental?: {};
}
/**
* @internal
*/
export type CreateRootTypes<TGenerics extends RootTypes> = TGenerics;
export type AnyRootTypes = CreateRootTypes<{
ctx: any;
meta: any;
errorShape: any;
transformer: any;
}>;
type PartialIf<TCondition extends boolean, TType> = TCondition extends true
? Partial<TType>
: TType;
/**
* Adds a `createContext` option with a given callback function
* If context is the default value, then the `createContext` option is optional
*/
export type CreateContextCallback<
TContext,
TFunction extends (...args: any[]) => any,
> = PartialIf<
object extends TContext ? true : false,
{
/**
* @see https://trpc.io/docs/v11/context
**/
createContext: TFunction;
}
>;

View File

@@ -0,0 +1,565 @@
import type { Observable } from '../observable';
import { createRecursiveProxy } from './createProxy';
import { defaultFormatter } from './error/formatter';
import { getTRPCErrorFromUnknown, TRPCError } from './error/TRPCError';
import type {
AnyProcedure,
ErrorHandlerOptions,
inferProcedureInput,
inferProcedureOutput,
LegacyObservableSubscriptionProcedure,
} from './procedure';
import type { ProcedureCallOptions } from './procedureBuilder';
import type { AnyRootTypes, RootConfig } from './rootConfig';
import { defaultTransformer } from './transformer';
import type { MaybePromise, ValueOf } from './types';
import {
emptyObject,
isFunction,
isObject,
mergeWithoutOverrides,
} from './utils';
export interface RouterRecord {
[key: string]: AnyProcedure | RouterRecord;
}
type DecorateProcedure<TProcedure extends AnyProcedure> = (
input: inferProcedureInput<TProcedure>,
) => Promise<
TProcedure['_def']['type'] extends 'subscription'
? TProcedure extends LegacyObservableSubscriptionProcedure<any>
? Observable<inferProcedureOutput<TProcedure>, TRPCError>
: inferProcedureOutput<TProcedure>
: inferProcedureOutput<TProcedure>
>;
/**
* @internal
*/
export type DecorateRouterRecord<TRecord extends RouterRecord> = {
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value
? $Value extends AnyProcedure
? DecorateProcedure<$Value>
: $Value extends RouterRecord
? DecorateRouterRecord<$Value>
: never
: never;
};
/**
* @internal
*/
export type RouterCallerErrorHandler<TContext> = (
opts: ErrorHandlerOptions<TContext>,
) => void;
/**
* @internal
*/
export type RouterCaller<
TRoot extends AnyRootTypes,
TRecord extends RouterRecord,
> = (
/**
* @note
* If passing a function, we recommend it's a cached function
* e.g. wrapped in `React.cache` to avoid unnecessary computations
*/
ctx: TRoot['ctx'] | (() => MaybePromise<TRoot['ctx']>),
options?: {
onError?: RouterCallerErrorHandler<TRoot['ctx']>;
signal?: AbortSignal;
},
) => DecorateRouterRecord<TRecord>;
/**
* @internal
*/
const lazyMarker = 'lazyMarker' as 'lazyMarker' & {
__brand: 'lazyMarker';
};
export type Lazy<TAny> = (() => Promise<TAny>) & { [lazyMarker]: true };
type LazyLoader<TAny> = {
load: () => Promise<void>;
ref: Lazy<TAny>;
};
function once<T>(fn: () => T): () => T {
const uncalled = Symbol();
let result: T | typeof uncalled = uncalled;
return (): T => {
if (result === uncalled) {
result = fn();
}
return result;
};
}
/**
* Lazy load a router
* @see https://trpc.io/docs/server/merging-routers#lazy-load
*/
export function lazy<TRouter extends AnyRouter>(
importRouter: () => Promise<
| TRouter
| {
[key: string]: TRouter;
}
>,
): Lazy<NoInfer<TRouter>> {
async function resolve(): Promise<TRouter> {
const mod = await importRouter();
// if the module is a router, return it
if (isRouter(mod)) {
return mod;
}
const routers = Object.values(mod);
if (routers.length !== 1 || !isRouter(routers[0])) {
throw new Error(
"Invalid router module - either define exactly 1 export or return the router directly.\nExample: `lazy(() => import('./slow.js').then((m) => m.slowRouter))`",
);
}
return routers[0];
}
(resolve as Lazy<NoInfer<TRouter>>)[lazyMarker] = true as const;
return resolve as Lazy<NoInfer<TRouter>>;
}
function isLazy<TAny>(input: unknown): input is Lazy<TAny> {
return typeof input === 'function' && lazyMarker in input;
}
/**
* @internal
*/
export interface RouterDef<
TRoot extends AnyRootTypes,
TRecord extends RouterRecord,
> {
_config: RootConfig<TRoot>;
router: true;
procedure?: never;
procedures: TRecord;
record: TRecord;
lazy: Record<string, LazyLoader<AnyRouter>>;
}
export interface Router<
TRoot extends AnyRootTypes,
TRecord extends RouterRecord,
> {
_def: RouterDef<TRoot, TRecord>;
/**
* @see https://trpc.io/docs/v11/server/server-side-calls
*/
createCaller: RouterCaller<TRoot, TRecord>;
}
export type BuiltRouter<
TRoot extends AnyRootTypes,
TRecord extends RouterRecord,
> = Router<TRoot, TRecord> & TRecord;
export interface RouterBuilder<TRoot extends AnyRootTypes> {
<TIn extends CreateRouterOptions>(
_: TIn,
): BuiltRouter<TRoot, DecorateCreateRouterOptions<TIn>>;
}
export type AnyRouter = Router<any, any>;
export type inferRouterRootTypes<TRouter extends AnyRouter> =
TRouter['_def']['_config']['$types'];
export type inferRouterContext<TRouter extends AnyRouter> =
inferRouterRootTypes<TRouter>['ctx'];
export type inferRouterError<TRouter extends AnyRouter> =
inferRouterRootTypes<TRouter>['errorShape'];
export type inferRouterMeta<TRouter extends AnyRouter> =
inferRouterRootTypes<TRouter>['meta'];
function isRouter(value: unknown): value is AnyRouter {
return (
isObject(value) && isObject(value['_def']) && 'router' in value['_def']
);
}
const emptyRouter = {
_ctx: null as any,
_errorShape: null as any,
_meta: null as any,
queries: {},
mutations: {},
subscriptions: {},
errorFormatter: defaultFormatter,
transformer: defaultTransformer,
};
/**
* Reserved words that can't be used as router or procedure names
*/
const reservedWords = [
/**
* Then is a reserved word because otherwise we can't return a promise that returns a Proxy
* since JS will think that `.then` is something that exists
*/
'then',
/**
* `fn.call()` and `fn.apply()` are reserved words because otherwise we can't call a function using `.call` or `.apply`
*/
'call',
'apply',
];
/** @internal */
export type CreateRouterOptions = {
[key: string]:
| AnyProcedure
| AnyRouter
| CreateRouterOptions
| Lazy<AnyRouter>;
};
/** @internal */
export type DecorateCreateRouterOptions<
TRouterOptions extends CreateRouterOptions,
> = {
[K in keyof TRouterOptions]: TRouterOptions[K] extends infer $Value
? $Value extends AnyProcedure
? $Value
: $Value extends Router<any, infer TRecord>
? TRecord
: $Value extends Lazy<Router<any, infer TRecord>>
? TRecord
: $Value extends CreateRouterOptions
? DecorateCreateRouterOptions<$Value>
: never
: never;
};
/**
* @internal
*/
export function createRouterFactory<TRoot extends AnyRootTypes>(
config: RootConfig<TRoot>,
) {
function createRouterInner<TInput extends CreateRouterOptions>(
input: TInput,
): BuiltRouter<TRoot, DecorateCreateRouterOptions<TInput>> {
const reservedWordsUsed = new Set(
Object.keys(input).filter((v) => reservedWords.includes(v)),
);
if (reservedWordsUsed.size > 0) {
throw new Error(
'Reserved words used in `router({})` call: ' +
Array.from(reservedWordsUsed).join(', '),
);
}
const procedures: Record<string, AnyProcedure> = emptyObject();
const lazy: Record<string, LazyLoader<AnyRouter>> = emptyObject();
function createLazyLoader(opts: {
ref: Lazy<AnyRouter>;
path: readonly string[];
key: string;
aggregate: RouterRecord;
}): LazyLoader<AnyRouter> {
return {
ref: opts.ref,
load: once(async () => {
const router = await opts.ref();
const lazyPath = [...opts.path, opts.key];
const lazyKey = lazyPath.join('.');
opts.aggregate[opts.key] = step(router._def.record, lazyPath);
delete lazy[lazyKey];
// add lazy loaders for nested routers
for (const [nestedKey, nestedItem] of Object.entries(
router._def.lazy,
)) {
const nestedRouterKey = [...lazyPath, nestedKey].join('.');
// console.log('adding lazy', nestedRouterKey);
lazy[nestedRouterKey] = createLazyLoader({
ref: nestedItem.ref,
path: lazyPath,
key: nestedKey,
aggregate: opts.aggregate[opts.key] as RouterRecord,
});
}
}),
};
}
function step(from: CreateRouterOptions, path: readonly string[] = []) {
const aggregate: RouterRecord = emptyObject();
for (const [key, item] of Object.entries(from ?? {})) {
if (isLazy(item)) {
lazy[[...path, key].join('.')] = createLazyLoader({
path,
ref: item,
key,
aggregate,
});
continue;
}
if (isRouter(item)) {
aggregate[key] = step(item._def.record, [...path, key]);
continue;
}
if (!isProcedure(item)) {
// RouterRecord
aggregate[key] = step(item, [...path, key]);
continue;
}
const newPath = [...path, key].join('.');
if (procedures[newPath]) {
throw new Error(`Duplicate key: ${newPath}`);
}
procedures[newPath] = item;
aggregate[key] = item;
}
return aggregate;
}
const record = step(input);
const _def: AnyRouter['_def'] = {
_config: config,
router: true,
procedures,
lazy,
...emptyRouter,
record,
};
const router: BuiltRouter<TRoot, {}> = {
...(record as {}),
_def,
createCaller: createCallerFactory<TRoot>()({
_def,
}),
};
return router as BuiltRouter<TRoot, DecorateCreateRouterOptions<TInput>>;
}
return createRouterInner;
}
function isProcedure(
procedureOrRouter: ValueOf<CreateRouterOptions>,
): procedureOrRouter is AnyProcedure {
return typeof procedureOrRouter === 'function';
}
/**
* @internal
*/
export async function getProcedureAtPath(
router: Pick<Router<any, any>, '_def'>,
path: string,
): Promise<AnyProcedure | null> {
const { _def } = router;
let procedure = _def.procedures[path];
while (!procedure) {
const key = Object.keys(_def.lazy).find((key) => path.startsWith(key));
// console.log(`found lazy: ${key ?? 'NOPE'} (fullPath: ${fullPath})`);
if (!key) {
return null;
}
// console.log('loading', key, '.......');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const lazyRouter = _def.lazy[key]!;
await lazyRouter.load();
procedure = _def.procedures[path];
}
return procedure;
}
/**
* @internal
*/
export async function callProcedure(
opts: ProcedureCallOptions<unknown> & {
router: AnyRouter;
allowMethodOverride?: boolean;
},
) {
const { type, path } = opts;
const proc = await getProcedureAtPath(opts.router, path);
if (
!proc ||
!isProcedure(proc) ||
(proc._def.type !== type && !opts.allowMethodOverride)
) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `No "${type}"-procedure on path "${path}"`,
});
}
/* istanbul ignore if -- @preserve */
if (
proc._def.type !== type &&
opts.allowMethodOverride &&
proc._def.type === 'subscription'
) {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: `Method override is not supported for subscriptions`,
});
}
return proc(opts);
}
export interface RouterCallerFactory<TRoot extends AnyRootTypes> {
<TRecord extends RouterRecord>(
router: Pick<Router<TRoot, TRecord>, '_def'>,
): RouterCaller<TRoot, TRecord>;
}
export function createCallerFactory<
TRoot extends AnyRootTypes,
>(): RouterCallerFactory<TRoot> {
return function createCallerInner<TRecord extends RouterRecord>(
router: Pick<Router<TRoot, TRecord>, '_def'>,
): RouterCaller<TRoot, TRecord> {
const { _def } = router;
type Context = TRoot['ctx'];
return function createCaller(ctxOrCallback, opts) {
return createRecursiveProxy<ReturnType<RouterCaller<any, any>>>(
async (innerOpts) => {
const { path, args } = innerOpts;
const fullPath = path.join('.');
if (path.length === 1 && path[0] === '_def') {
return _def;
}
const procedure = await getProcedureAtPath(router, fullPath);
let ctx: Context | undefined = undefined;
try {
if (!procedure) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `No procedure found on path "${path}"`,
});
}
ctx = isFunction(ctxOrCallback)
? await Promise.resolve(ctxOrCallback())
: ctxOrCallback;
return await procedure({
path: fullPath,
getRawInput: async () => args[0],
ctx,
type: procedure._def.type,
signal: opts?.signal,
batchIndex: 0,
});
} catch (cause) {
opts?.onError?.({
ctx,
error: getTRPCErrorFromUnknown(cause),
input: args[0],
path: fullPath,
type: procedure?._def.type ?? 'unknown',
});
throw cause;
}
},
);
};
};
}
/** @internal */
export type MergeRouters<
TRouters extends AnyRouter[],
TRoot extends AnyRootTypes = TRouters[0]['_def']['_config']['$types'],
TRecord extends RouterRecord = {},
> = TRouters extends [
infer Head extends AnyRouter,
...infer Tail extends AnyRouter[],
]
? MergeRouters<Tail, TRoot, Head['_def']['record'] & TRecord>
: BuiltRouter<TRoot, TRecord>;
export function mergeRouters<TRouters extends AnyRouter[]>(
...routerList: [...TRouters]
): MergeRouters<TRouters> {
const record = mergeWithoutOverrides(
{},
...routerList.map((r) => r._def.record),
);
const errorFormatter = routerList.reduce(
(currentErrorFormatter, nextRouter) => {
if (
nextRouter._def._config.errorFormatter &&
nextRouter._def._config.errorFormatter !== defaultFormatter
) {
if (
currentErrorFormatter !== defaultFormatter &&
currentErrorFormatter !== nextRouter._def._config.errorFormatter
) {
throw new Error('You seem to have several error formatters');
}
return nextRouter._def._config.errorFormatter;
}
return currentErrorFormatter;
},
defaultFormatter,
);
const transformer = routerList.reduce((prev, current) => {
if (
current._def._config.transformer &&
current._def._config.transformer !== defaultTransformer
) {
if (
prev !== defaultTransformer &&
prev !== current._def._config.transformer
) {
throw new Error('You seem to have several transformers');
}
return current._def._config.transformer;
}
return prev;
}, defaultTransformer);
const router = createRouterFactory({
errorFormatter,
transformer,
isDev: routerList.every((r) => r._def._config.isDev),
allowOutsideOfServer: routerList.every(
(r) => r._def._config.allowOutsideOfServer,
),
isServer: routerList.every((r) => r._def._config.isServer),
$types: routerList[0]?._def._config.$types,
sse: routerList[0]?._def._config.sse,
})(record);
return router as MergeRouters<TRouters>;
}

View File

@@ -0,0 +1,81 @@
import type { InvertKeyValue, ValueOf } from '../types';
// reference: https://www.jsonrpc.org/specification
/**
* JSON-RPC 2.0 Error codes
*
* `-32000` to `-32099` are reserved for implementation-defined server-errors.
* For tRPC we're copying the last digits of HTTP 4XX errors.
*/
export const TRPC_ERROR_CODES_BY_KEY = {
/**
* Invalid JSON was received by the server.
* An error occurred on the server while parsing the JSON text.
*/
PARSE_ERROR: -32700,
/**
* The JSON sent is not a valid Request object.
*/
BAD_REQUEST: -32600, // 400
// Internal JSON-RPC error
INTERNAL_SERVER_ERROR: -32603, // 500
NOT_IMPLEMENTED: -32603, // 501
BAD_GATEWAY: -32603, // 502
SERVICE_UNAVAILABLE: -32603, // 503
GATEWAY_TIMEOUT: -32603, // 504
// Implementation specific errors
UNAUTHORIZED: -32001, // 401
PAYMENT_REQUIRED: -32002, // 402
FORBIDDEN: -32003, // 403
NOT_FOUND: -32004, // 404
METHOD_NOT_SUPPORTED: -32005, // 405
TIMEOUT: -32008, // 408
CONFLICT: -32009, // 409
PRECONDITION_FAILED: -32012, // 412
PAYLOAD_TOO_LARGE: -32013, // 413
UNSUPPORTED_MEDIA_TYPE: -32015, // 415
UNPROCESSABLE_CONTENT: -32022, // 422
PRECONDITION_REQUIRED: -32028, // 428
TOO_MANY_REQUESTS: -32029, // 429
CLIENT_CLOSED_REQUEST: -32099, // 499
} as const;
// pure
export const TRPC_ERROR_CODES_BY_NUMBER: InvertKeyValue<
typeof TRPC_ERROR_CODES_BY_KEY
> = {
[-32700]: 'PARSE_ERROR',
[-32600]: 'BAD_REQUEST',
[-32603]: 'INTERNAL_SERVER_ERROR',
[-32001]: 'UNAUTHORIZED',
[-32002]: 'PAYMENT_REQUIRED',
[-32003]: 'FORBIDDEN',
[-32004]: 'NOT_FOUND',
[-32005]: 'METHOD_NOT_SUPPORTED',
[-32008]: 'TIMEOUT',
[-32009]: 'CONFLICT',
[-32012]: 'PRECONDITION_FAILED',
[-32013]: 'PAYLOAD_TOO_LARGE',
[-32015]: 'UNSUPPORTED_MEDIA_TYPE',
[-32022]: 'UNPROCESSABLE_CONTENT',
[-32028]: 'PRECONDITION_REQUIRED',
[-32029]: 'TOO_MANY_REQUESTS',
[-32099]: 'CLIENT_CLOSED_REQUEST',
};
export type TRPC_ERROR_CODE_NUMBER = ValueOf<typeof TRPC_ERROR_CODES_BY_KEY>;
export type TRPC_ERROR_CODE_KEY = keyof typeof TRPC_ERROR_CODES_BY_KEY;
/**
* tRPC error codes that are considered retryable
* With out of the box SSE, the client will reconnect when these errors are encountered
*/
export const retryableRpcCodes: TRPC_ERROR_CODE_NUMBER[] = [
TRPC_ERROR_CODES_BY_KEY.BAD_GATEWAY,
TRPC_ERROR_CODES_BY_KEY.SERVICE_UNAVAILABLE,
TRPC_ERROR_CODES_BY_KEY.GATEWAY_TIMEOUT,
TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR,
];

View File

@@ -0,0 +1,153 @@
/* eslint-disable @typescript-eslint/no-namespace */
import type { TRPCRequestInfo } from '../http/types';
import type { ProcedureType } from '../procedure';
import type { TRPC_ERROR_CODE_NUMBER } from './codes';
/**
* Error response
*/
export interface TRPCErrorShape<TData extends object = object> {
code: TRPC_ERROR_CODE_NUMBER;
message: string;
data: TData;
}
/**
* JSON-RPC 2.0 Specification
*/
export namespace JSONRPC2 {
export type RequestId = number | string | null;
/**
* All requests/responses extends this shape
*/
export interface BaseEnvelope {
id?: RequestId;
jsonrpc?: '2.0';
}
export interface BaseRequest<TMethod extends string = string>
extends BaseEnvelope {
method: TMethod;
}
export interface Request<TMethod extends string = string, TParams = unknown>
extends BaseRequest<TMethod> {
params: TParams;
}
export interface ResultResponse<TResult = unknown> extends BaseEnvelope {
result: TResult;
}
export interface ErrorResponse<TError extends TRPCErrorShape = TRPCErrorShape>
extends BaseEnvelope {
error: TError;
}
}
/////////////////////////// HTTP envelopes ///////////////////////
export interface TRPCRequest
extends JSONRPC2.Request<
ProcedureType,
{
path: string;
input: unknown;
/**
* The last event id that the client received
*/
lastEventId?: string;
}
> {}
export interface TRPCResult<TData = unknown> {
data: TData;
type?: 'data';
/**
* The id of the message to keep track of in case of a reconnect
*/
id?: string;
}
export interface TRPCSuccessResponse<TData>
extends JSONRPC2.ResultResponse<TRPCResult<TData>> {}
export interface TRPCErrorResponse<
TError extends TRPCErrorShape = TRPCErrorShape,
> extends JSONRPC2.ErrorResponse<TError> {}
export type TRPCResponse<
TData = unknown,
TError extends TRPCErrorShape = TRPCErrorShape,
> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>;
/////////////////////////// WebSocket envelopes ///////////////////////
export type TRPCRequestMessage = TRPCRequest & {
id: JSONRPC2.RequestId;
};
/**
* The client asked the server to unsubscribe
*/
export interface TRPCSubscriptionStopNotification
extends JSONRPC2.BaseRequest<'subscription.stop'> {
id: null;
}
/**
* The client's outgoing request types
*/
export type TRPCClientOutgoingRequest = TRPCSubscriptionStopNotification;
/**
* The client's sent messages shape
*/
export type TRPCClientOutgoingMessage =
| TRPCRequestMessage
| (JSONRPC2.BaseRequest<'subscription.stop'> & { id: JSONRPC2.RequestId });
export interface TRPCResultMessage<TData>
extends JSONRPC2.ResultResponse<
| { type: 'started'; data?: never }
| { type: 'stopped'; data?: never }
| TRPCResult<TData>
> {}
export type TRPCResponseMessage<
TData = unknown,
TError extends TRPCErrorShape = TRPCErrorShape,
> = { id: JSONRPC2.RequestId } & (
| TRPCErrorResponse<TError>
| TRPCResultMessage<TData>
);
/**
* The server asked the client to reconnect - useful when restarting/redeploying service
*/
export interface TRPCReconnectNotification
extends JSONRPC2.BaseRequest<'reconnect'> {
id: JSONRPC2.RequestId;
}
/**
* The client's incoming request types
*/
export type TRPCClientIncomingRequest = TRPCReconnectNotification;
/**
* The client's received messages shape
*/
export type TRPCClientIncomingMessage<
TResult = unknown,
TError extends TRPCErrorShape = TRPCErrorShape,
> = TRPCClientIncomingRequest | TRPCResponseMessage<TResult, TError>;
/**
* The client sends connection params - always sent as the first message
*/
export interface TRPCConnectionParamsMessage
extends JSONRPC2.BaseRequest<'connectionParams'> {
data: TRPCRequestInfo['connectionParams'];
}

View File

@@ -0,0 +1,26 @@
export {
TRPC_ERROR_CODES_BY_KEY,
TRPC_ERROR_CODES_BY_NUMBER,
retryableRpcCodes,
} from './codes';
export type { TRPC_ERROR_CODE_KEY, TRPC_ERROR_CODE_NUMBER } from './codes';
export type {
JSONRPC2,
TRPCClientIncomingMessage,
TRPCClientIncomingRequest,
TRPCClientOutgoingMessage,
TRPCClientOutgoingRequest,
TRPCErrorResponse,
TRPCErrorShape,
TRPCReconnectNotification,
TRPCRequest,
TRPCRequestMessage,
TRPCResponse,
TRPCResponseMessage,
TRPCResult,
TRPCResultMessage,
TRPCSubscriptionStopNotification,
TRPCSuccessResponse,
TRPCConnectionParamsMessage,
} from './envelopes';
export { parseTRPCMessage } from './parseTRPCMessage';

View File

@@ -0,0 +1,89 @@
import { procedureTypes, type ProcedureType } from '../procedure';
import type { CombinedDataTransformer } from '../transformer';
import { isObject } from '../utils';
import type { TRPCClientOutgoingMessage } from './envelopes';
/* istanbul ignore next -- @preserve */
function assertIsObject(obj: unknown): asserts obj is Record<string, unknown> {
if (!isObject(obj)) {
throw new Error('Not an object');
}
}
/* istanbul ignore next -- @preserve */
function assertIsProcedureType(obj: unknown): asserts obj is ProcedureType {
if (!procedureTypes.includes(obj as any)) {
throw new Error('Invalid procedure type');
}
}
/* istanbul ignore next -- @preserve */
function assertIsRequestId(
obj: unknown,
): asserts obj is number | string | null {
if (
obj !== null &&
typeof obj === 'number' &&
isNaN(obj) &&
typeof obj !== 'string'
) {
throw new Error('Invalid request id');
}
}
/* istanbul ignore next -- @preserve */
function assertIsString(obj: unknown): asserts obj is string {
if (typeof obj !== 'string') {
throw new Error('Invalid string');
}
}
/* istanbul ignore next -- @preserve */
function assertIsJSONRPC2OrUndefined(
obj: unknown,
): asserts obj is '2.0' | undefined {
if (typeof obj !== 'undefined' && obj !== '2.0') {
throw new Error('Must be JSONRPC 2.0');
}
}
/** @public */
export function parseTRPCMessage(
obj: unknown,
transformer: CombinedDataTransformer,
): TRPCClientOutgoingMessage {
assertIsObject(obj);
const { id, jsonrpc, method, params } = obj;
assertIsRequestId(id);
assertIsJSONRPC2OrUndefined(jsonrpc);
if (method === 'subscription.stop') {
return {
id,
jsonrpc,
method,
};
}
assertIsProcedureType(method);
assertIsObject(params);
const { input: rawInput, path, lastEventId } = params;
assertIsString(path);
if (lastEventId !== undefined) {
assertIsString(lastEventId);
}
const input = transformer.input.deserialize(rawInput);
return {
id,
jsonrpc,
method,
params: {
input,
path,
lastEventId,
},
};
}

View File

@@ -0,0 +1,654 @@
import { isPlainObject } from '@trpc/server/vendor/is-plain-object';
import {
emptyObject,
isAsyncIterable,
isFunction,
isObject,
run,
} from '../utils';
import { iteratorResource } from './utils/asyncIterable';
import type { Deferred } from './utils/createDeferred';
import { createDeferred } from './utils/createDeferred';
import { makeResource } from './utils/disposable';
import { mergeAsyncIterables } from './utils/mergeAsyncIterables';
import { readableStreamFrom } from './utils/readableStreamFrom';
import { PING_SYM, withPing } from './utils/withPing';
/**
* A subset of the standard ReadableStream properties needed by tRPC internally.
* @see ReadableStream from lib.dom.d.ts
*/
export type WebReadableStreamEsque = {
getReader: () => ReadableStreamDefaultReader<Uint8Array>;
};
export type NodeJSReadableStreamEsque = {
on(
eventName: string | symbol,
listener: (...args: any[]) => void,
): NodeJSReadableStreamEsque;
};
// ---------- types
const CHUNK_VALUE_TYPE_PROMISE = 0;
type CHUNK_VALUE_TYPE_PROMISE = typeof CHUNK_VALUE_TYPE_PROMISE;
const CHUNK_VALUE_TYPE_ASYNC_ITERABLE = 1;
type CHUNK_VALUE_TYPE_ASYNC_ITERABLE = typeof CHUNK_VALUE_TYPE_ASYNC_ITERABLE;
const PROMISE_STATUS_FULFILLED = 0;
type PROMISE_STATUS_FULFILLED = typeof PROMISE_STATUS_FULFILLED;
const PROMISE_STATUS_REJECTED = 1;
type PROMISE_STATUS_REJECTED = typeof PROMISE_STATUS_REJECTED;
const ASYNC_ITERABLE_STATUS_RETURN = 0;
type ASYNC_ITERABLE_STATUS_RETURN = typeof ASYNC_ITERABLE_STATUS_RETURN;
const ASYNC_ITERABLE_STATUS_YIELD = 1;
type ASYNC_ITERABLE_STATUS_YIELD = typeof ASYNC_ITERABLE_STATUS_YIELD;
const ASYNC_ITERABLE_STATUS_ERROR = 2;
type ASYNC_ITERABLE_STATUS_ERROR = typeof ASYNC_ITERABLE_STATUS_ERROR;
type ChunkDefinitionKey =
// root should be replaced
| null
// at array path
| number
// at key path
| string;
type ChunkIndex = number & { __chunkIndex: true };
type ChunkValueType =
| CHUNK_VALUE_TYPE_PROMISE
| CHUNK_VALUE_TYPE_ASYNC_ITERABLE;
type ChunkDefinition = [
key: ChunkDefinitionKey,
type: ChunkValueType,
chunkId: ChunkIndex,
];
type EncodedValue = [
// data
[unknown] | [],
// chunk descriptions
...ChunkDefinition[],
];
type Head = Record<string, EncodedValue>;
type PromiseChunk =
| [
chunkIndex: ChunkIndex,
status: PROMISE_STATUS_FULFILLED,
value: EncodedValue,
]
| [chunkIndex: ChunkIndex, status: PROMISE_STATUS_REJECTED, error: unknown];
type IterableChunk =
| [
chunkIndex: ChunkIndex,
status: ASYNC_ITERABLE_STATUS_RETURN,
value: EncodedValue,
]
| [
chunkIndex: ChunkIndex,
status: ASYNC_ITERABLE_STATUS_YIELD,
value: EncodedValue,
]
| [
chunkIndex: ChunkIndex,
status: ASYNC_ITERABLE_STATUS_ERROR,
error: unknown,
];
type ChunkData = PromiseChunk | IterableChunk;
type PlaceholderValue = 0 & { __placeholder: true };
export function isPromise(value: unknown): value is Promise<unknown> {
return (
(isObject(value) || isFunction(value)) &&
typeof value?.['then'] === 'function' &&
typeof value?.['catch'] === 'function'
);
}
type Serialize = (value: any) => any;
type Deserialize = (value: any) => any;
type PathArray = readonly (string | number)[];
export type ProducerOnError = (opts: {
error: unknown;
path: PathArray;
}) => void;
export interface JSONLProducerOptions {
serialize?: Serialize;
data: Record<string, unknown> | unknown[];
onError?: ProducerOnError;
formatError?: (opts: { error: unknown; path: PathArray }) => unknown;
maxDepth?: number;
/**
* Interval in milliseconds to send a ping to the client to keep the connection alive
* This will be sent as a whitespace character
* @default undefined
*/
pingMs?: number;
}
class MaxDepthError extends Error {
constructor(public path: (string | number)[]) {
super('Max depth reached at path: ' + path.join('.'));
}
}
async function* createBatchStreamProducer(
opts: JSONLProducerOptions,
): AsyncIterable<Head | ChunkData | typeof PING_SYM, void> {
const { data } = opts;
let counter = 0 as ChunkIndex;
const placeholder = 0 as PlaceholderValue;
const mergedIterables = mergeAsyncIterables<ChunkData>();
function registerAsync(
callback: (idx: ChunkIndex) => AsyncIterable<ChunkData, void>,
) {
const idx = counter++ as ChunkIndex;
const iterable = callback(idx);
mergedIterables.add(iterable);
return idx;
}
function encodePromise(promise: Promise<unknown>, path: (string | number)[]) {
return registerAsync(async function* (idx) {
const error = checkMaxDepth(path);
if (error) {
// Catch any errors from the original promise to ensure they're reported
promise.catch((cause) => {
opts.onError?.({ error: cause, path });
});
// Replace the promise with a rejected one containing the max depth error
promise = Promise.reject(error);
}
try {
const next = await promise;
yield [idx, PROMISE_STATUS_FULFILLED, encode(next, path)];
} catch (cause) {
opts.onError?.({ error: cause, path });
yield [
idx,
PROMISE_STATUS_REJECTED,
opts.formatError?.({ error: cause, path }),
];
}
});
}
function encodeAsyncIterable(
iterable: AsyncIterable<unknown>,
path: (string | number)[],
) {
return registerAsync(async function* (idx) {
const error = checkMaxDepth(path);
if (error) {
throw error;
}
await using iterator = iteratorResource(iterable);
try {
while (true) {
const next = await iterator.next();
if (next.done) {
yield [idx, ASYNC_ITERABLE_STATUS_RETURN, encode(next.value, path)];
break;
}
yield [idx, ASYNC_ITERABLE_STATUS_YIELD, encode(next.value, path)];
}
} catch (cause) {
opts.onError?.({ error: cause, path });
yield [
idx,
ASYNC_ITERABLE_STATUS_ERROR,
opts.formatError?.({ error: cause, path }),
];
}
});
}
function checkMaxDepth(path: (string | number)[]) {
if (opts.maxDepth && path.length > opts.maxDepth) {
return new MaxDepthError(path);
}
return null;
}
function encodeAsync(
value: unknown,
path: (string | number)[],
): null | [type: ChunkValueType, chunkId: ChunkIndex] {
if (isPromise(value)) {
return [CHUNK_VALUE_TYPE_PROMISE, encodePromise(value, path)];
}
if (isAsyncIterable(value)) {
if (opts.maxDepth && path.length >= opts.maxDepth) {
throw new Error('Max depth reached');
}
return [
CHUNK_VALUE_TYPE_ASYNC_ITERABLE,
encodeAsyncIterable(value, path),
];
}
return null;
}
function encode(value: unknown, path: (string | number)[]): EncodedValue {
if (value === undefined) {
return [[]];
}
const reg = encodeAsync(value, path);
if (reg) {
return [[placeholder], [null, ...reg]];
}
if (!isPlainObject(value)) {
return [[value]];
}
const newObj: Record<string, unknown> = emptyObject();
const asyncValues: ChunkDefinition[] = [];
for (const [key, item] of Object.entries(value)) {
const transformed = encodeAsync(item, [...path, key]);
if (!transformed) {
newObj[key] = item;
continue;
}
newObj[key] = placeholder;
asyncValues.push([key, ...transformed]);
}
return [[newObj], ...asyncValues];
}
const newHead: Head = emptyObject();
for (const [key, item] of Object.entries(data)) {
newHead[key] = encode(item, [key]);
}
yield newHead;
let iterable: AsyncIterable<ChunkData | typeof PING_SYM, void> =
mergedIterables;
if (opts.pingMs) {
iterable = withPing(mergedIterables, opts.pingMs);
}
for await (const value of iterable) {
yield value;
}
}
/**
* JSON Lines stream producer
* @see https://jsonlines.org/
*/
export function jsonlStreamProducer(opts: JSONLProducerOptions) {
let stream = readableStreamFrom(createBatchStreamProducer(opts));
const { serialize } = opts;
if (serialize) {
stream = stream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
if (chunk === PING_SYM) {
controller.enqueue(PING_SYM);
} else {
controller.enqueue(serialize(chunk));
}
},
}),
);
}
return stream
.pipeThrough(
new TransformStream({
transform(chunk, controller) {
if (chunk === PING_SYM) {
controller.enqueue(' ');
} else {
controller.enqueue(JSON.stringify(chunk) + '\n');
}
},
}),
)
.pipeThrough(new TextEncoderStream());
}
class AsyncError extends Error {
constructor(public readonly data: unknown) {
super('Received error from server');
}
}
export type ConsumerOnError = (opts: { error: unknown }) => void;
const nodeJsStreamToReaderEsque = (source: NodeJSReadableStreamEsque) => {
return {
getReader() {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
source.on('data', (chunk) => {
controller.enqueue(chunk);
});
source.on('end', () => {
controller.close();
});
source.on('error', (error) => {
controller.error(error);
});
},
});
return stream.getReader();
},
};
};
function createLineAccumulator(
from: NodeJSReadableStreamEsque | WebReadableStreamEsque,
) {
const reader =
'getReader' in from
? from.getReader()
: nodeJsStreamToReaderEsque(from).getReader();
let lineAggregate = '';
return new ReadableStream({
async pull(controller) {
const { done, value } = await reader.read();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
cancel() {
return reader.cancel();
},
})
.pipeThrough(new TextDecoderStream())
.pipeThrough(
new TransformStream<string, string>({
transform(chunk, controller) {
lineAggregate += chunk;
const parts = lineAggregate.split('\n');
lineAggregate = parts.pop() ?? '';
for (const part of parts) {
controller.enqueue(part);
}
},
}),
);
}
function createConsumerStream<THead>(
from: NodeJSReadableStreamEsque | WebReadableStreamEsque,
) {
const stream = createLineAccumulator(from);
let sentHead = false;
return stream.pipeThrough(
new TransformStream<string, ChunkData | THead>({
transform(line, controller) {
if (!sentHead) {
const head = JSON.parse(line);
controller.enqueue(head as THead);
sentHead = true;
} else {
const chunk: ChunkData = JSON.parse(line);
controller.enqueue(chunk);
}
},
}),
);
}
/**
* Creates a handler for managing stream controllers and their lifecycle
*/
function createStreamsManager(abortController: AbortController) {
const controllerMap = new Map<
ChunkIndex,
ReturnType<typeof createStreamController>
>();
/**
* Checks if there are no pending controllers or deferred promises
*/
function isEmpty() {
return Array.from(controllerMap.values()).every((c) => c.closed);
}
/**
* Creates a stream controller
*/
function createStreamController() {
let originalController: ReadableStreamDefaultController<ChunkData>;
const stream = new ReadableStream<ChunkData>({
start(controller) {
originalController = controller;
},
});
const streamController = {
enqueue: (v: ChunkData) => originalController.enqueue(v),
close: () => {
originalController.close();
clear();
if (isEmpty()) {
abortController.abort();
}
},
closed: false,
getReaderResource: () => {
const reader = stream.getReader();
return makeResource(reader, () => {
streamController.close();
reader.releaseLock();
});
},
error: (reason: unknown) => {
originalController.error(reason);
clear();
},
};
function clear() {
Object.assign(streamController, {
closed: true,
close: () => {
// noop
},
enqueue: () => {
// noop
},
getReaderResource: null,
error: () => {
// noop
},
});
}
return streamController;
}
/**
* Gets or creates a stream controller
*/
function getOrCreate(chunkId: ChunkIndex) {
let c = controllerMap.get(chunkId);
if (!c) {
c = createStreamController();
controllerMap.set(chunkId, c);
}
return c;
}
/**
* Cancels all pending controllers and rejects deferred promises
*/
function cancelAll(reason: unknown) {
for (const controller of controllerMap.values()) {
controller.error(reason);
}
}
/**
* Closes all pending controllers to preserve buffered data
*/
function closeAll() {
for (const controller of controllerMap.values()) {
controller.close();
}
}
return {
getOrCreate,
cancelAll,
closeAll,
};
}
/**
* JSON Lines stream consumer
* @see https://jsonlines.org/
*/
export async function jsonlStreamConsumer<THead>(opts: {
from: NodeJSReadableStreamEsque | WebReadableStreamEsque;
deserialize?: Deserialize;
onError?: ConsumerOnError;
formatError?: (opts: { error: unknown }) => Error;
/**
* This `AbortController` will be triggered when there are no more listeners to the stream.
*/
abortController: AbortController;
}) {
const { deserialize = (v) => v } = opts;
let source = createConsumerStream<Head>(opts.from);
if (deserialize) {
source = source.pipeThrough(
new TransformStream({
transform(chunk, controller) {
controller.enqueue(deserialize(chunk));
},
}),
);
}
let headDeferred: null | Deferred<THead> = createDeferred();
const streamManager = createStreamsManager(opts.abortController);
function decodeChunkDefinition(value: ChunkDefinition) {
const [_path, type, chunkId] = value;
const controller = streamManager.getOrCreate(chunkId);
switch (type) {
case CHUNK_VALUE_TYPE_PROMISE: {
return run(async () => {
using reader = controller.getReaderResource();
const { value } = await reader.read();
const [_chunkId, status, data] = value as PromiseChunk;
switch (status) {
case PROMISE_STATUS_FULFILLED:
return decode(data);
case PROMISE_STATUS_REJECTED:
throw opts.formatError?.({ error: data }) ?? new AsyncError(data);
}
});
}
case CHUNK_VALUE_TYPE_ASYNC_ITERABLE: {
return run(async function* () {
using reader = controller.getReaderResource();
while (true) {
const { value } = await reader.read();
const [_chunkId, status, data] = value as IterableChunk;
switch (status) {
case ASYNC_ITERABLE_STATUS_YIELD:
yield decode(data);
break;
case ASYNC_ITERABLE_STATUS_RETURN:
return decode(data);
case ASYNC_ITERABLE_STATUS_ERROR:
throw (
opts.formatError?.({ error: data }) ?? new AsyncError(data)
);
}
}
});
}
}
}
function decode(value: EncodedValue): unknown {
const [[data], ...asyncProps] = value;
for (const value of asyncProps) {
const [key] = value;
const decoded = decodeChunkDefinition(value);
if (key === null) {
return decoded;
}
(data as any)[key] = decoded;
}
return data;
}
const handleClose = () => {
// If the stream closes before emitting any head data,
// we need to reject the headDeferred to prevent hanging
if (headDeferred) {
headDeferred.reject(new Error('Stream closed before head was received'));
headDeferred = null;
}
// Close stream controllers (not error them)
// to preserve any buffered chunks
streamManager.closeAll();
};
const handleAbort = (reason?: unknown) => {
headDeferred?.reject(reason);
headDeferred = null;
streamManager.cancelAll(reason);
};
source
.pipeTo(
new WritableStream({
write(chunkOrHead) {
if (headDeferred) {
const head = chunkOrHead as Record<number | string, unknown>;
for (const [key, value] of Object.entries(chunkOrHead)) {
const parsed = decode(value as any);
head[key] = parsed;
}
headDeferred.resolve(head as THead);
headDeferred = null;
return;
}
const chunk = chunkOrHead as ChunkData;
const [idx] = chunk;
const controller = streamManager.getOrCreate(idx);
controller.enqueue(chunk);
},
close: handleClose,
abort: handleAbort,
}),
)
.catch((error) => {
opts.onError?.({ error });
handleAbort(error);
});
return [await headDeferred.promise] as const;
}

View File

@@ -0,0 +1,454 @@
import { Unpromise } from '../../vendor/unpromise';
import { getTRPCErrorFromUnknown } from '../error/TRPCError';
import { isAbortError } from '../http/abortError';
import type { MaybePromise } from '../types';
import { emptyObject, identity, run } from '../utils';
import type { EventSourceLike } from './sse.types';
import type { inferTrackedOutput } from './tracked';
import { isTrackedEnvelope } from './tracked';
import { takeWithGrace } from './utils/asyncIterable';
import { makeAsyncResource } from './utils/disposable';
import { readableStreamFrom } from './utils/readableStreamFrom';
import {
disposablePromiseTimerResult,
timerResource,
} from './utils/timerResource';
import { PING_SYM, withPing } from './utils/withPing';
type Serialize = (value: any) => any;
type Deserialize = (value: any) => any;
/**
* @internal
*/
export interface SSEPingOptions {
/**
* Enable ping comments sent from the server
* @default false
*/
enabled: boolean;
/**
* Interval in milliseconds
* @default 1000
*/
intervalMs?: number;
}
export interface SSEClientOptions {
/**
* Timeout and reconnect after inactivity in milliseconds
* @default undefined
*/
reconnectAfterInactivityMs?: number;
}
export interface SSEStreamProducerOptions<TValue = unknown> {
serialize?: Serialize;
data: AsyncIterable<TValue>;
maxDepth?: number;
ping?: SSEPingOptions;
/**
* Maximum duration in milliseconds for the request before ending the stream
* @default undefined
*/
maxDurationMs?: number;
/**
* End the request immediately after data is sent
* Only useful for serverless runtimes that do not support streaming responses
* @default false
*/
emitAndEndImmediately?: boolean;
formatError?: (opts: { error: unknown }) => unknown;
/**
* Client-specific options - these will be sent to the client as part of the first message
* @default {}
*/
client?: SSEClientOptions;
}
const PING_EVENT = 'ping';
const SERIALIZED_ERROR_EVENT = 'serialized-error';
const CONNECTED_EVENT = 'connected';
const RETURN_EVENT = 'return';
interface SSEvent {
id?: string;
data: unknown;
comment?: string;
event?: string;
}
/**
*
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html
*/
export function sseStreamProducer<TValue = unknown>(
opts: SSEStreamProducerOptions<TValue>,
) {
const { serialize = identity } = opts;
const ping: Required<SSEPingOptions> = {
enabled: opts.ping?.enabled ?? false,
intervalMs: opts.ping?.intervalMs ?? 1000,
};
const client: SSEClientOptions = opts.client ?? {};
if (
ping.enabled &&
client.reconnectAfterInactivityMs &&
ping.intervalMs > client.reconnectAfterInactivityMs
) {
throw new Error(
`Ping interval must be less than client reconnect interval to prevent unnecessary reconnection - ping.intervalMs: ${ping.intervalMs} client.reconnectAfterInactivityMs: ${client.reconnectAfterInactivityMs}`,
);
}
async function* generator(): AsyncIterable<SSEvent, void> {
yield {
event: CONNECTED_EVENT,
data: JSON.stringify(client),
};
type TIteratorValue = Awaited<TValue> | typeof PING_SYM;
let iterable: AsyncIterable<TValue | typeof PING_SYM> = opts.data;
if (opts.emitAndEndImmediately) {
iterable = takeWithGrace(iterable, {
count: 1,
gracePeriodMs: 1,
});
}
if (ping.enabled && ping.intervalMs !== Infinity && ping.intervalMs > 0) {
iterable = withPing(iterable, ping.intervalMs);
}
// We need those declarations outside the loop for garbage collection reasons. If they were
// declared inside, they would not be freed until the next value is present.
let value: null | TIteratorValue;
let chunk: null | SSEvent;
for await (value of iterable) {
if (value === PING_SYM) {
yield { event: PING_EVENT, data: '' };
continue;
}
chunk = isTrackedEnvelope(value)
? { id: value[0], data: value[1] }
: { data: value };
chunk.data = JSON.stringify(serialize(chunk.data));
yield chunk;
// free up references for garbage collection
value = null;
chunk = null;
}
}
async function* generatorWithErrorHandling(): AsyncIterable<SSEvent, void> {
try {
yield* generator();
yield {
event: RETURN_EVENT,
data: '',
};
} catch (cause) {
if (isAbortError(cause)) {
// ignore abort errors, send any other errors
return;
}
// `err` must be caused by `opts.data`, `JSON.stringify` or `serialize`.
// So, a user error in any case.
const error = getTRPCErrorFromUnknown(cause);
const data = opts.formatError?.({ error }) ?? null;
yield {
event: SERIALIZED_ERROR_EVENT,
data: JSON.stringify(serialize(data)),
};
}
}
const stream = readableStreamFrom(generatorWithErrorHandling());
return stream
.pipeThrough(
new TransformStream({
transform(chunk, controller: TransformStreamDefaultController<string>) {
if ('event' in chunk) {
controller.enqueue(`event: ${chunk.event}\n`);
}
if ('data' in chunk) {
controller.enqueue(`data: ${chunk.data}\n`);
}
if ('id' in chunk) {
controller.enqueue(`id: ${chunk.id}\n`);
}
if ('comment' in chunk) {
controller.enqueue(`: ${chunk.comment}\n`);
}
controller.enqueue('\n\n');
},
}),
)
.pipeThrough(new TextEncoderStream());
}
interface ConsumerStreamResultBase<TConfig extends ConsumerConfig> {
eventSource: InstanceType<TConfig['EventSource']> | null;
}
interface ConsumerStreamResultData<TConfig extends ConsumerConfig>
extends ConsumerStreamResultBase<TConfig> {
type: 'data';
data: inferTrackedOutput<TConfig['data']>;
}
interface ConsumerStreamResultError<TConfig extends ConsumerConfig>
extends ConsumerStreamResultBase<TConfig> {
type: 'serialized-error';
error: TConfig['error'];
}
interface ConsumerStreamResultConnecting<TConfig extends ConsumerConfig>
extends ConsumerStreamResultBase<TConfig> {
type: 'connecting';
event: EventSourceLike.EventOf<TConfig['EventSource']> | null;
}
interface ConsumerStreamResultTimeout<TConfig extends ConsumerConfig>
extends ConsumerStreamResultBase<TConfig> {
type: 'timeout';
ms: number;
}
interface ConsumerStreamResultPing<TConfig extends ConsumerConfig>
extends ConsumerStreamResultBase<TConfig> {
type: 'ping';
}
interface ConsumerStreamResultConnected<TConfig extends ConsumerConfig>
extends ConsumerStreamResultBase<TConfig> {
type: 'connected';
options: SSEClientOptions;
}
type ConsumerStreamResult<TConfig extends ConsumerConfig> =
| ConsumerStreamResultData<TConfig>
| ConsumerStreamResultError<TConfig>
| ConsumerStreamResultConnecting<TConfig>
| ConsumerStreamResultTimeout<TConfig>
| ConsumerStreamResultPing<TConfig>
| ConsumerStreamResultConnected<TConfig>;
export interface SSEStreamConsumerOptions<TConfig extends ConsumerConfig> {
url: () => MaybePromise<string>;
init: () =>
| MaybePromise<EventSourceLike.InitDictOf<TConfig['EventSource']>>
| undefined;
signal: AbortSignal;
deserialize?: Deserialize;
EventSource: TConfig['EventSource'];
}
interface ConsumerConfig {
data: unknown;
error: unknown;
EventSource: EventSourceLike.AnyConstructor;
}
async function withTimeout<T>(opts: {
promise: Promise<T>;
timeoutMs: number;
onTimeout: () => Promise<NoInfer<T>>;
}): Promise<T> {
using timeoutPromise = timerResource(opts.timeoutMs);
const res = await Unpromise.race([opts.promise, timeoutPromise.start()]);
if (res === disposablePromiseTimerResult) {
return await opts.onTimeout();
}
return res;
}
/**
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html
*/
export function sseStreamConsumer<TConfig extends ConsumerConfig>(
opts: SSEStreamConsumerOptions<TConfig>,
): AsyncIterable<ConsumerStreamResult<TConfig>> {
const { deserialize = (v) => v } = opts;
let clientOptions: SSEClientOptions = emptyObject();
const signal = opts.signal;
let _es: InstanceType<TConfig['EventSource']> | null = null;
const createStream = () =>
new ReadableStream<ConsumerStreamResult<TConfig>>({
async start(controller) {
const [url, init] = await Promise.all([opts.url(), opts.init()]);
const eventSource = (_es = new opts.EventSource(
url,
init,
) as InstanceType<TConfig['EventSource']>);
controller.enqueue({
type: 'connecting',
eventSource: _es,
event: null,
});
eventSource.addEventListener(CONNECTED_EVENT, (_msg) => {
const msg = _msg as EventSourceLike.MessageEvent;
const options: SSEClientOptions = JSON.parse(msg.data);
clientOptions = options;
controller.enqueue({
type: 'connected',
options,
eventSource,
});
});
eventSource.addEventListener(SERIALIZED_ERROR_EVENT, (_msg) => {
const msg = _msg as EventSourceLike.MessageEvent;
controller.enqueue({
type: 'serialized-error',
error: deserialize(JSON.parse(msg.data)),
eventSource,
});
});
eventSource.addEventListener(PING_EVENT, () => {
controller.enqueue({
type: 'ping',
eventSource,
});
});
eventSource.addEventListener(RETURN_EVENT, () => {
eventSource.close();
controller.close();
_es = null;
});
eventSource.addEventListener('error', (event) => {
if (eventSource.readyState === eventSource.CLOSED) {
controller.error(event);
} else {
controller.enqueue({
type: 'connecting',
eventSource,
event,
});
}
});
eventSource.addEventListener('message', (_msg) => {
const msg = _msg as EventSourceLike.MessageEvent;
const chunk = deserialize(JSON.parse(msg.data));
const def: SSEvent = {
data: chunk,
};
if (msg.lastEventId) {
def.id = msg.lastEventId;
}
controller.enqueue({
type: 'data',
data: def as inferTrackedOutput<TConfig['data']>,
eventSource,
});
});
const onAbort = () => {
try {
eventSource.close();
controller.close();
} catch {
// ignore errors in case the controller is already closed
}
};
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener('abort', onAbort);
}
},
cancel() {
_es?.close();
},
});
const getStreamResource = () => {
let stream = createStream();
let reader = stream.getReader();
async function dispose() {
await reader.cancel();
_es = null;
}
return makeAsyncResource(
{
read() {
return reader.read();
},
async recreate() {
await dispose();
stream = createStream();
reader = stream.getReader();
},
},
dispose,
);
};
return run(async function* () {
await using stream = getStreamResource();
while (true) {
let promise = stream.read();
const timeoutMs = clientOptions.reconnectAfterInactivityMs;
if (timeoutMs) {
promise = withTimeout({
promise,
timeoutMs,
onTimeout: async () => {
const res: Awaited<typeof promise> = {
value: {
type: 'timeout',
ms: timeoutMs,
eventSource: _es,
},
done: false,
};
// Close and release old reader
await stream.recreate();
return res;
},
});
}
const result = await promise;
if (result.done) {
return result.value;
}
yield result.value;
}
});
}
export const sseHeaders = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'X-Accel-Buffering': 'no',
Connection: 'keep-alive',
} as const;

View File

@@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-namespace */
/**
* @internal
*/
export namespace EventSourceLike {
export interface InitDict {
withCredentials?: boolean;
}
export interface MessageEvent extends Event {
data: any;
lastEventId?: string;
}
export interface Event {}
type EventSourceListenerLike = (event: Event) => void;
export type AnyConstructorLike<TInit extends InitDict> = new (
url: string,
eventSourceInitDict?: TInit,
) => Instance;
export interface Instance {
readonly CLOSED: number;
readonly CONNECTING: number;
readonly OPEN: number;
addEventListener(type: string, listener: EventSourceListenerLike): void;
removeEventListener(type: string, listener: EventSourceListenerLike): void;
close: () => void;
readyState: number;
}
export type AnyConstructor = AnyConstructorLike<any>;
export type ListenerOf<T extends AnyConstructor> = Parameters<
InstanceType<T>['addEventListener']
>[1];
export type EventOf<T extends AnyConstructor> = Parameters<ListenerOf<T>>[0];
export type InitDictOf<T extends AnyConstructor> =
ConstructorParameters<T>[1];
}

View File

@@ -0,0 +1,49 @@
const trackedSymbol = Symbol();
type TrackedId = string & {
__brand: 'TrackedId';
};
export type TrackedEnvelope<TData> = [TrackedId, TData, typeof trackedSymbol];
export interface TrackedData<TData> {
/**
* The id of the message to keep track of in case the connection gets lost
*/
id: string;
/**
* The data field of the message
*/
data: TData;
}
/**
* Produce a typed server-sent event message
* @deprecated use `tracked(id, data)` instead
*/
export function sse<TData>(event: { id: string; data: TData }) {
return tracked(event.id, event.data);
}
export function isTrackedEnvelope<TData>(
value: unknown,
): value is TrackedEnvelope<TData> {
return Array.isArray(value) && value[2] === trackedSymbol;
}
/**
* Automatically track an event so that it can be resumed from a given id if the connection is lost
*/
export function tracked<TData>(
id: string,
data: TData,
): TrackedEnvelope<TData> {
if (id === '') {
// This limitation could be removed by using different SSE event names / channels for tracked event and non-tracked event
throw new Error(
'`id` must not be an empty string as empty string is the same as not setting the id at all',
);
}
return [id as TrackedId, data, trackedSymbol];
}
export type inferTrackedOutput<TData> =
TData extends TrackedEnvelope<infer $Data> ? TrackedData<$Data> : TData;

View File

@@ -0,0 +1,62 @@
import { Unpromise } from '../../../vendor/unpromise';
import { throwAbortError } from '../../http/abortError';
import { makeAsyncResource } from './disposable';
import { disposablePromiseTimerResult, timerResource } from './timerResource';
export function iteratorResource<TYield, TReturn, TNext>(
iterable: AsyncIterable<TYield, TReturn, TNext>,
): AsyncIterator<TYield, TReturn, TNext> & AsyncDisposable {
const iterator = iterable[Symbol.asyncIterator]();
// @ts-expect-error - this is added in node 24 which we don't officially support yet
// eslint-disable-next-line no-restricted-syntax
if (iterator[Symbol.asyncDispose]) {
return iterator as AsyncIterator<TYield, TReturn, TNext> & AsyncDisposable;
}
return makeAsyncResource(iterator, async () => {
await iterator.return?.();
});
}
/**
* Derives a new {@link AsyncGenerator} based of {@link iterable}, that yields its first
* {@link count} values. Then, a grace period of {@link gracePeriodMs} is started in which further
* values may still come through. After this period, the generator aborts.
*/
export async function* takeWithGrace<T>(
iterable: AsyncIterable<T>,
opts: {
count: number;
gracePeriodMs: number;
},
): AsyncGenerator<T> {
await using iterator = iteratorResource(iterable);
// declaration outside the loop for garbage collection reasons
let result: null | IteratorResult<T> | typeof disposablePromiseTimerResult;
using timer = timerResource(opts.gracePeriodMs);
let count = opts.count;
let timerPromise = new Promise<typeof disposablePromiseTimerResult>(() => {
// never resolves
});
while (true) {
result = await Unpromise.race([iterator.next(), timerPromise]);
if (result === disposablePromiseTimerResult) {
throwAbortError();
}
if (result.done) {
return result.value;
}
yield result.value;
if (--count === 0) {
timerPromise = timer.start();
}
// free up reference for garbage collection
result = null;
}
}

View File

@@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
export function createDeferred<TValue = void>() {
let resolve: (value: TValue) => void;
let reject: (error: unknown) => void;
const promise = new Promise<TValue>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve: resolve!, reject: reject! };
}
export type Deferred<TValue> = ReturnType<typeof createDeferred<TValue>>;

View File

@@ -0,0 +1,54 @@
// @ts-expect-error - polyfilling symbol
// eslint-disable-next-line no-restricted-syntax
Symbol.dispose ??= Symbol();
// @ts-expect-error - polyfilling symbol
// eslint-disable-next-line no-restricted-syntax
Symbol.asyncDispose ??= Symbol();
/**
* Takes a value and a dispose function and returns a new object that implements the Disposable interface.
* The returned object is the original value augmented with a Symbol.dispose method.
* @param thing The value to make disposable
* @param dispose Function to call when disposing the resource
* @returns The original value with Symbol.dispose method added
*/
export function makeResource<T>(thing: T, dispose: () => void): T & Disposable {
const it = thing as T & Partial<Disposable>;
// eslint-disable-next-line no-restricted-syntax
const existing = it[Symbol.dispose];
// eslint-disable-next-line no-restricted-syntax
it[Symbol.dispose] = () => {
dispose();
existing?.();
};
return it as T & Disposable;
}
/**
* Takes a value and an async dispose function and returns a new object that implements the AsyncDisposable interface.
* The returned object is the original value augmented with a Symbol.asyncDispose method.
* @param thing The value to make async disposable
* @param dispose Async function to call when disposing the resource
* @returns The original value with Symbol.asyncDispose method added
*/
export function makeAsyncResource<T>(
thing: T,
dispose: () => Promise<void>,
): T & AsyncDisposable {
const it = thing as T & Partial<AsyncDisposable>;
// eslint-disable-next-line no-restricted-syntax
const existing = it[Symbol.asyncDispose];
// eslint-disable-next-line no-restricted-syntax
it[Symbol.asyncDispose] = async () => {
await dispose();
await existing?.();
};
return it as T & AsyncDisposable;
}

View File

@@ -0,0 +1,193 @@
import { createDeferred } from './createDeferred';
import { makeAsyncResource } from './disposable';
type ManagedIteratorResult<TYield, TReturn> =
| { status: 'yield'; value: TYield }
| { status: 'return'; value: TReturn }
| { status: 'error'; error: unknown };
function createManagedIterator<TYield, TReturn>(
iterable: AsyncIterable<TYield, TReturn>,
onResult: (result: ManagedIteratorResult<TYield, TReturn>) => void,
) {
const iterator = iterable[Symbol.asyncIterator]();
let state: 'idle' | 'pending' | 'done' = 'idle';
function cleanup() {
state = 'done';
onResult = () => {
// noop
};
}
function pull() {
if (state !== 'idle') {
return;
}
state = 'pending';
const next = iterator.next();
next
.then((result) => {
if (result.done) {
state = 'done';
onResult({ status: 'return', value: result.value });
cleanup();
return;
}
state = 'idle';
onResult({ status: 'yield', value: result.value });
})
.catch((cause) => {
onResult({ status: 'error', error: cause });
cleanup();
});
}
return {
pull,
destroy: async () => {
cleanup();
await iterator.return?.();
},
};
}
type ManagedIterator<TYield, TReturn> = ReturnType<
typeof createManagedIterator<TYield, TReturn>
>;
interface MergedAsyncIterables<TYield>
extends AsyncIterable<TYield, void, unknown> {
add(iterable: AsyncIterable<TYield>): void;
}
/**
* Creates a new async iterable that merges multiple async iterables into a single stream.
* Values from the input iterables are yielded in the order they resolve, similar to Promise.race().
*
* New iterables can be added dynamically using the returned {@link MergedAsyncIterables.add} method, even after iteration has started.
*
* If any of the input iterables throws an error, that error will be propagated through the merged stream.
* Other iterables will not continue to be processed.
*
* @template TYield The type of values yielded by the input iterables
*/
export function mergeAsyncIterables<TYield>(): MergedAsyncIterables<TYield> {
let state: 'idle' | 'pending' | 'done' = 'idle';
let flushSignal = createDeferred();
/**
* used while {@link state} is `idle`
*/
const iterables: AsyncIterable<TYield, void, unknown>[] = [];
/**
* used while {@link state} is `pending`
*/
const iterators = new Set<ManagedIterator<TYield, void>>();
const buffer: Array<
[
iterator: ManagedIterator<TYield, void>,
result: Exclude<
ManagedIteratorResult<TYield, void>,
{ status: 'return' }
>,
]
> = [];
function initIterable(iterable: AsyncIterable<TYield, void, unknown>) {
if (state !== 'pending') {
// shouldn't happen
return;
}
const iterator = createManagedIterator(iterable, (result) => {
if (state !== 'pending') {
// shouldn't happen
return;
}
switch (result.status) {
case 'yield':
buffer.push([iterator, result]);
break;
case 'return':
iterators.delete(iterator);
break;
case 'error':
buffer.push([iterator, result]);
iterators.delete(iterator);
break;
}
flushSignal.resolve();
});
iterators.add(iterator);
iterator.pull();
}
return {
add(iterable: AsyncIterable<TYield, void, unknown>) {
switch (state) {
case 'idle':
iterables.push(iterable);
break;
case 'pending':
initIterable(iterable);
break;
case 'done': {
// shouldn't happen
break;
}
}
},
async *[Symbol.asyncIterator]() {
if (state !== 'idle') {
throw new Error('Cannot iterate twice');
}
state = 'pending';
await using _finally = makeAsyncResource({}, async () => {
state = 'done';
const errors: unknown[] = [];
await Promise.all(
Array.from(iterators.values()).map(async (it) => {
try {
await it.destroy();
} catch (cause) {
errors.push(cause);
}
}),
);
buffer.length = 0;
iterators.clear();
flushSignal.resolve();
if (errors.length > 0) {
throw new AggregateError(errors);
}
});
while (iterables.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
initIterable(iterables.shift()!);
}
while (iterators.size > 0) {
await flushSignal.promise;
while (buffer.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [iterator, result] = buffer.shift()!;
switch (result.status) {
case 'yield':
yield result.value;
iterator.pull();
break;
case 'error':
throw result.error;
}
}
flushSignal = createDeferred();
}
},
};
}

View File

@@ -0,0 +1,28 @@
/**
* Creates a ReadableStream from an AsyncIterable.
*
* @param iterable - The source AsyncIterable to stream from
* @returns A ReadableStream that yields values from the AsyncIterable
*/
export function readableStreamFrom<TYield>(
iterable: AsyncIterable<TYield, void>,
): ReadableStream<TYield> {
const iterator = iterable[Symbol.asyncIterator]();
return new ReadableStream({
async cancel() {
await iterator.return?.();
},
async pull(controller) {
const result = await iterator.next();
if (result.done) {
controller.close();
return;
}
controller.enqueue(result.value);
},
});
}

View File

@@ -0,0 +1,29 @@
import { makeResource } from './disposable';
export const disposablePromiseTimerResult = Symbol();
export function timerResource(ms: number) {
let timer: ReturnType<typeof setTimeout> | null = null;
return makeResource(
{
start() {
if (timer) {
throw new Error('Timer already started');
}
const promise = new Promise<typeof disposablePromiseTimerResult>(
(resolve) => {
timer = setTimeout(() => resolve(disposablePromiseTimerResult), ms);
},
);
return promise;
},
},
() => {
if (timer) {
clearTimeout(timer);
}
},
);
}

View File

@@ -0,0 +1,47 @@
import { Unpromise } from '../../../vendor/unpromise';
import { iteratorResource } from './asyncIterable';
import { disposablePromiseTimerResult, timerResource } from './timerResource';
export const PING_SYM = Symbol('ping');
/**
* Derives a new {@link AsyncGenerator} based of {@link iterable}, that yields {@link PING_SYM}
* whenever no value has been yielded for {@link pingIntervalMs}.
*/
export async function* withPing<TValue>(
iterable: AsyncIterable<TValue>,
pingIntervalMs: number,
): AsyncGenerator<TValue | typeof PING_SYM> {
await using iterator = iteratorResource(iterable);
// declaration outside the loop for garbage collection reasons
let result:
| null
| IteratorResult<TValue>
| typeof disposablePromiseTimerResult;
let nextPromise = iterator.next();
while (true) {
using pingPromise = timerResource(pingIntervalMs);
result = await Unpromise.race([nextPromise, pingPromise.start()]);
if (result === disposablePromiseTimerResult) {
// cancelled
yield PING_SYM;
continue;
}
if (result.done) {
return result.value;
}
nextPromise = iterator.next();
yield result.value;
// free up reference for garbage collection
result = null;
}
}

View File

@@ -0,0 +1,194 @@
import type { AnyRootTypes, RootConfig } from './rootConfig';
import type { AnyRouter, inferRouterError } from './router';
import type {
TRPCResponse,
TRPCResponseMessage,
TRPCResultMessage,
} from './rpc';
import { isObject } from './utils';
/**
* @public
*/
export interface DataTransformer {
serialize(object: any): any;
deserialize(object: any): any;
}
interface InputDataTransformer extends DataTransformer {
/**
* This function runs **on the client** before sending the data to the server.
*/
serialize(object: any): any;
/**
* This function runs **on the server** to transform the data before it is passed to the resolver
*/
deserialize(object: any): any;
}
interface OutputDataTransformer extends DataTransformer {
/**
* This function runs **on the server** before sending the data to the client.
*/
serialize(object: any): any;
/**
* This function runs **only on the client** to transform the data sent from the server.
*/
deserialize(object: any): any;
}
/**
* @public
*/
export interface CombinedDataTransformer {
/**
* Specify how the data sent from the client to the server should be transformed.
*/
input: InputDataTransformer;
/**
* Specify how the data sent from the server to the client should be transformed.
*/
output: OutputDataTransformer;
}
/**
* @public
*/
export type CombinedDataTransformerClient = {
input: Pick<CombinedDataTransformer['input'], 'serialize'>;
output: Pick<CombinedDataTransformer['output'], 'deserialize'>;
};
/**
* @public
*/
export type DataTransformerOptions = CombinedDataTransformer | DataTransformer;
/**
* @internal
*/
export function getDataTransformer(
transformer: DataTransformerOptions,
): CombinedDataTransformer {
if ('input' in transformer) {
return transformer;
}
return { input: transformer, output: transformer };
}
/**
* @internal
*/
export const defaultTransformer: CombinedDataTransformer = {
input: { serialize: (obj) => obj, deserialize: (obj) => obj },
output: { serialize: (obj) => obj, deserialize: (obj) => obj },
};
function transformTRPCResponseItem<
TResponseItem extends TRPCResponse | TRPCResponseMessage,
>(config: RootConfig<AnyRootTypes>, item: TResponseItem): TResponseItem {
if ('error' in item) {
return {
...item,
error: config.transformer.output.serialize(item.error),
};
}
if ('data' in item.result) {
return {
...item,
result: {
...item.result,
data: config.transformer.output.serialize(item.result.data),
},
};
}
return item;
}
/**
* Takes a unserialized `TRPCResponse` and serializes it with the router's transformers
**/
export function transformTRPCResponse<
TResponse extends
| TRPCResponse
| TRPCResponse[]
| TRPCResponseMessage
| TRPCResponseMessage[],
>(config: RootConfig<AnyRootTypes>, itemOrItems: TResponse) {
return Array.isArray(itemOrItems)
? itemOrItems.map((item) => transformTRPCResponseItem(config, item))
: transformTRPCResponseItem(config, itemOrItems);
}
// FIXME:
// - the generics here are probably unnecessary
// - the RPC-spec could probably be simplified to combine HTTP + WS
/** @internal */
function transformResultInner<TRouter extends AnyRouter, TOutput>(
response:
| TRPCResponse<TOutput, inferRouterError<TRouter>>
| TRPCResponseMessage<TOutput, inferRouterError<TRouter>>,
transformer: DataTransformer,
) {
if ('error' in response) {
const error = transformer.deserialize(
response.error,
) as inferRouterError<TRouter>;
return {
ok: false,
error: {
...response,
error,
},
} as const;
}
const result = {
...response.result,
...((!response.result.type || response.result.type === 'data') && {
type: 'data',
data: transformer.deserialize(response.result.data),
}),
} as TRPCResultMessage<TOutput>['result'];
return { ok: true, result } as const;
}
class TransformResultError extends Error {
constructor() {
super('Unable to transform response from server');
}
}
/**
* Transforms and validates that the result is a valid TRPCResponse
* @internal
*/
export function transformResult<TRouter extends AnyRouter, TOutput>(
response:
| TRPCResponse<TOutput, inferRouterError<TRouter>>
| TRPCResponseMessage<TOutput, inferRouterError<TRouter>>,
transformer: DataTransformer,
): ReturnType<typeof transformResultInner> {
let result: ReturnType<typeof transformResultInner>;
try {
// Use the data transformers on the JSON-response
result = transformResultInner(response, transformer);
} catch {
throw new TransformResultError();
}
// check that output of the transformers is a valid TRPCResponse
if (
!result.ok &&
(!isObject(result.error.error) ||
typeof result.error.error['code'] !== 'number')
) {
throw new TransformResultError();
}
if (result.ok && !isObject(result.result)) {
throw new TransformResultError();
}
return result;
}

View File

@@ -0,0 +1,185 @@
/**
* ================================
* Useful utility types that doesn't have anything to do with tRPC in particular
* ================================
*/
/**
* @public
*/
export type Maybe<TType> = TType | null | undefined;
/**
* @internal
* @see https://github.com/ianstormtaylor/superstruct/blob/7973400cd04d8ad92bbdc2b6f35acbfb3c934079/src/utils.ts#L323-L325
*/
export type Simplify<TType> = TType extends any[] | Date
? TType
: { [K in keyof TType]: TType[K] };
/**
* @public
*/
export type Dict<TType> = Record<string, TType | undefined>;
/**
* @public
*/
export type MaybePromise<TType> = Promise<TType> | TType;
export type FilterKeys<TObj extends object, TFilter> = {
[TKey in keyof TObj]: TObj[TKey] extends TFilter ? TKey : never;
}[keyof TObj];
/**
* @internal
*/
export type Result<TType, TErr = unknown> =
| { ok: true; value: TType }
| { ok: false; error: TErr };
/**
* @internal
*/
export type Filter<TObj extends object, TFilter> = Pick<
TObj,
FilterKeys<TObj, TFilter>
>;
/**
* Unwrap return type if the type is a function (sync or async), else use the type as is
* @internal
*/
export type Unwrap<TType> = TType extends (...args: any[]) => infer R
? Awaited<R>
: TType;
/**
* Makes the object recursively optional
* @internal
*/
export type DeepPartial<TObject> = TObject extends object
? {
[P in keyof TObject]?: DeepPartial<TObject[P]>;
}
: TObject;
/**
* Omits the key without removing a potential union
* @internal
*/
export type DistributiveOmit<TObj, TKey extends keyof any> = TObj extends any
? Omit<TObj, TKey>
: never;
/**
* See https://github.com/microsoft/TypeScript/issues/41966#issuecomment-758187996
* Fixes issues with iterating over keys of objects with index signatures.
* Without this, iterations over keys of objects with index signatures will lose
* type information about the keys and only the index signature will remain.
* @internal
*/
export type WithoutIndexSignature<TObj> = {
[K in keyof TObj as string extends K
? never
: number extends K
? never
: K]: TObj[K];
};
/**
* @internal
* Overwrite properties in `TType` with properties in `TWith`
* Only overwrites properties when the type to be overwritten
* is an object. Otherwise it will just use the type from `TWith`.
*/
export type Overwrite<TType, TWith> = TWith extends any
? TType extends object
? {
[K in // Exclude index signature from keys
| keyof WithoutIndexSignature<TType>
| keyof WithoutIndexSignature<TWith>]: K extends keyof TWith
? TWith[K]
: K extends keyof TType
? TType[K]
: never;
} & (string extends keyof TWith // Handle cases with an index signature
? { [key: string]: TWith[string] }
: number extends keyof TWith
? { [key: number]: TWith[number] }
: {})
: TWith
: never;
/**
* @internal
*/
export type ValidateShape<TActualShape, TExpectedShape> =
TActualShape extends TExpectedShape
? Exclude<keyof TActualShape, keyof TExpectedShape> extends never
? TActualShape
: TExpectedShape
: never;
/**
* @internal
*/
export type PickFirstDefined<TType, TPick> = undefined extends TType
? undefined extends TPick
? never
: TPick
: TType;
export type KeyFromValue<
TValue,
TType extends Record<PropertyKey, PropertyKey>,
> = {
[K in keyof TType]: TValue extends TType[K] ? K : never;
}[keyof TType];
export type InvertKeyValue<TType extends Record<PropertyKey, PropertyKey>> = {
[TValue in TType[keyof TType]]: KeyFromValue<TValue, TType>;
};
/**
* ================================
* tRPC specific types
* ================================
*/
/**
* @internal
*/
export type IntersectionError<TKey extends string> =
`The property '${TKey}' in your router collides with a built-in method, rename this router or procedure on your backend.`;
/**
* @internal
*/
export type ProtectedIntersection<TType, TWith> = keyof TType &
keyof TWith extends never
? TType & TWith
: IntersectionError<string & keyof TType & keyof TWith>;
/**
* @internal
* Returns the raw input type of a procedure
*/
export type GetRawInputFn = () => Promise<unknown>;
const _errorSymbol = Symbol();
export type ErrorSymbol = typeof _errorSymbol;
export type TypeError<TMessage extends string> = TMessage & {
_: typeof _errorSymbol;
};
export type ValueOf<TObj> = TObj[keyof TObj];
export type coerceAsyncIterableToArray<TValue> =
TValue extends AsyncIterable<infer $Inferred> ? $Inferred[] : TValue;
/**
* @internal
* Infers the type of the value yielded by an async iterable
*/
export type inferAsyncIterableYield<T> =
T extends AsyncIterable<infer U> ? U : T;

View File

@@ -0,0 +1,117 @@
/** @internal */
export type UnsetMarker = 'unsetMarker' & {
__brand: 'unsetMarker';
};
/**
* Ensures there are no duplicate keys when building a procedure.
* @internal
*/
export function mergeWithoutOverrides<TType extends Record<string, unknown>>(
obj1: TType,
...objs: Partial<TType>[]
): TType {
const newObj: TType = Object.assign(emptyObject(), obj1);
for (const overrides of objs) {
for (const key in overrides) {
if (key in newObj && newObj[key] !== overrides[key]) {
throw new Error(`Duplicate key ${key}`);
}
newObj[key as keyof TType] = overrides[key] as TType[keyof TType];
}
}
return newObj;
}
/**
* Check that value is object
* @internal
*/
export function isObject(value: unknown): value is Record<string, unknown> {
return !!value && !Array.isArray(value) && typeof value === 'object';
}
type AnyFn = ((...args: any[]) => unknown) & Record<keyof any, unknown>;
export function isFunction(fn: unknown): fn is AnyFn {
return typeof fn === 'function';
}
/**
* Create an object without inheriting anything from `Object.prototype`
* @internal
*/
export function emptyObject<TObj extends Record<string, unknown>>(): TObj {
return Object.create(null);
}
const asyncIteratorsSupported =
typeof Symbol === 'function' && !!Symbol.asyncIterator;
export function isAsyncIterable<TValue>(
value: unknown,
): value is AsyncIterable<TValue> {
return (
asyncIteratorsSupported && isObject(value) && Symbol.asyncIterator in value
);
}
/**
* Run an IIFE
*/
export const run = <TValue>(fn: () => TValue): TValue => fn();
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function noop(): void {}
export function identity<T>(it: T): T {
return it;
}
/**
* Generic runtime assertion function. Throws, if the condition is not `true`.
*
* Can be used as a slightly less dangerous variant of type assertions. Code
* mistakes would be revealed at runtime then (hopefully during testing).
*/
export function assert(
condition: boolean,
msg = 'no additional info',
): asserts condition {
if (!condition) {
throw new Error(`AssertionError: ${msg}`);
}
}
export function sleep(ms = 0): Promise<void> {
return new Promise<void>((res) => setTimeout(res, ms));
}
/**
* Ponyfill for
* [`AbortSignal.any`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static).
*/
export function abortSignalsAnyPonyfill(signals: AbortSignal[]): AbortSignal {
if (typeof AbortSignal.any === 'function') {
return AbortSignal.any(signals);
}
const ac = new AbortController();
for (const signal of signals) {
if (signal.aborted) {
trigger();
break;
}
signal.addEventListener('abort', trigger, { once: true });
}
return ac.signal;
function trigger() {
ac.abort();
for (const signal of signals) {
signal.removeEventListener('abort', trigger);
}
}
}

View File

@@ -0,0 +1,91 @@
/**
* Based on https://github.com/unjs/cookie-es/tree/v1.2.2
* MIT License
*
* Cookie-es copyright (c) Pooya Parsa <pooya@pi0.io>
* Set-Cookie parsing based on https://github.com/nfriedly/set-cookie-parser
* Copyright (c) 2015 Nathan Friedly <nathan@nfriedly.com> (http://nfriedly.com/)
*
* @see https://github.com/unjs/cookie-es/blob/main/src/set-cookie/split.ts
*/
/**
* Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
* that are within a single set-cookie field-value, such as in the Expires portion.
*
* See https://tools.ietf.org/html/rfc2616#section-4.2
*/
export function splitSetCookieString(
cookiesString: string | string[],
): string[] {
if (Array.isArray(cookiesString)) {
return cookiesString.flatMap((c) => splitSetCookieString(c));
}
if (typeof cookiesString !== "string") {
return [];
}
const cookiesStrings: string[] = [];
let pos: number = 0;
let start: number;
let ch: string;
let lastComma: number;
let nextStart: number;
let cookiesSeparatorFound: boolean;
const skipWhitespace = () => {
while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
pos += 1;
}
return pos < cookiesString.length;
};
const notSpecialChar = () => {
ch = cookiesString.charAt(pos);
return ch !== "=" && ch !== ";" && ch !== ",";
};
while (pos < cookiesString.length) {
start = pos;
cookiesSeparatorFound = false;
while (skipWhitespace()) {
ch = cookiesString.charAt(pos);
if (ch === ",") {
// ',' is a cookie separator if we have later first '=', not ';' or ','
lastComma = pos;
pos += 1;
skipWhitespace();
nextStart = pos;
while (pos < cookiesString.length && notSpecialChar()) {
pos += 1;
}
// currently special character
if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
// we found cookies separator
cookiesSeparatorFound = true;
// pos is inside the next cookie, so back up and return it.
pos = nextStart;
cookiesStrings.push(cookiesString.slice(start, lastComma));
start = pos;
} else {
// in param ',' or param separator ';',
// we continue from that comma
pos = lastComma + 1;
}
} else {
pos += 1;
}
}
if (!cookiesSeparatorFound || pos >= cookiesString.length) {
cookiesStrings.push(cookiesString.slice(start));
}
}
return cookiesStrings;
}

View File

@@ -0,0 +1,32 @@
/*!
* is-plain-object <https://github.com/jonschlinkert/is-plain-object>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
function isObject(o: unknown): o is Record<string, unknown> {
return Object.prototype.toString.call(o) === '[object Object]';
}
export function isPlainObject(o: unknown): o is Record<string, unknown> {
var ctor,prot;
if (isObject(o) === false) return false;
// If has modified constructor
ctor = o.constructor;
if (ctor === undefined) return true;
// If has modified prototype
prot = ctor.prototype;
if (isObject(prot) === false) return false;
// If constructor does not have an Object-specific method
if (prot.hasOwnProperty('isPrototypeOf') === false) {
return false;
}
// Most likely a plain Object
return true;
};

View File

@@ -0,0 +1,19 @@
import type { StandardSchemaV1 } from "./spec";
/** A schema error with useful information. */
export class StandardSchemaV1Error extends Error {
/** The schema issues. */
public readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
/**
* Creates a schema error with useful information.
*
* @param issues The schema issues.
*/
constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>) {
super(issues[0]?.message);
this.name = 'SchemaError';
this.issues = issues;
}
}

View File

@@ -0,0 +1,80 @@
/**
*
* @see https://github.com/standard-schema/standard-schema/blob/main/packages/spec/src/index.ts
*/
/* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/naming-convention */
/** The Standard Schema interface. */
export interface StandardSchemaV1<Input = unknown, Output = Input> {
/** The Standard Schema properties. */
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
}
export declare namespace StandardSchemaV1 {
/** The Standard Schema properties interface. */
export interface Props<Input = unknown, Output = Input> {
/** The version number of the standard. */
readonly version: 1;
/** The vendor name of the schema library. */
readonly vendor: string;
/** Validates unknown input values. */
readonly validate: (
value: unknown,
) => Result<Output> | Promise<Result<Output>>;
/** Inferred types associated with the schema. */
readonly types?: Types<Input, Output> | undefined;
}
/** The result interface of the validate function. */
export type Result<Output> = SuccessResult<Output> | FailureResult;
/** The result interface if validation succeeds. */
export interface SuccessResult<Output> {
/** The typed output value. */
readonly value: Output;
/** The non-existent issues. */
readonly issues?: undefined;
}
/** The result interface if validation fails. */
export interface FailureResult {
/** The issues of failed validation. */
readonly issues: ReadonlyArray<Issue>;
}
/** The issue interface of the failure output. */
export interface Issue {
/** The error message of the issue. */
readonly message: string;
/** The path of the issue, if any. */
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}
/** The path segment interface of the issue. */
export interface PathSegment {
/** The key representing a path segment. */
readonly key: PropertyKey;
}
/** The Standard Schema types interface. */
export interface Types<Input = unknown, Output = Input> {
/** The input type of the schema. */
readonly input: Input;
/** The output type of the schema. */
readonly output: Output;
}
/** Infers the input type of a Standard Schema. */
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
Schema['~standard']['types']
>['input'];
/** Infers the output type of a Standard Schema. */
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
Schema['~standard']['types']
>['output'];
}

View File

@@ -0,0 +1 @@
Origin source: https://github.com/cefn/watchable/tree/main/packages/unpromise

20
node_modules/@trpc/server/src/vendor/unpromise/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,20 @@
MIT License
Copyright 2023 Cefn Hoile
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,7 @@
export { Unpromise } from "./unpromise";
export type {
ProxyPromise,
SubscribedPromise,
PromiseExecutor,
PromiseWithResolvers,
} from "./types";

View File

@@ -0,0 +1,55 @@
/** TYPES */
/** A promise that exploits a single, memory-safe upstream subscription
* to a single re-used Unpromise that persists for the VM lifetime of a
* Promise.
*
* Calling unsubscribe() removes the subscription, eliminating
* all references to the SubscribedPromise. */
export interface SubscribedPromise<T> extends Promise<T> {
unsubscribe: () => void;
}
/** Duplicate of Promise interface, except each call returns SubscribedPromise */
export interface ProxyPromise<T> extends Promise<T> {
subscribe: () => SubscribedPromise<T>;
then: <TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| null
,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| null
) => SubscribedPromise<TResult1 | TResult2>;
catch: <TResult = never>(
onrejected?:
| ((reason: any) => TResult | PromiseLike<TResult>)
| null
) => SubscribedPromise<T | TResult>;
finally: (
onfinally?: (() => void) | null
) => SubscribedPromise<T>;
}
export type PromiseExecutor<T> = (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void
) => void;
/** A standard pattern for a resolvable, rejectable Promise, based
* on the emerging ES2023 standard. Type ported from
* https://github.com/microsoft/TypeScript/pull/56593 */
export interface PromiseWithResolvers<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
/** Given an array, this is the union of its members' types. */
// export type MemberOf<Arr extends readonly unknown[]> = Arr[number];

View File

@@ -0,0 +1,380 @@
/* eslint-disable @typescript-eslint/unbound-method */
import type {
PromiseExecutor,
PromiseWithResolvers,
ProxyPromise,
SubscribedPromise,
} from "./types";
/** Memory safe (weakmapped) cache of the ProxyPromise for each Promise,
* which is retained for the lifetime of the original Promise.
*/
const subscribableCache = new WeakMap<
PromiseLike<unknown>,
ProxyPromise<unknown>
>();
/** A NOOP function allowing a consistent interface for settled
* SubscribedPromises (settled promises are not subscribed - they resolve
* immediately). */
const NOOP = () => {
// noop
};
/**
* Every `Promise<T>` can be shadowed by a single `ProxyPromise<T>`. It is
* created once, cached and reused throughout the lifetime of the Promise. Get a
* Promise's ProxyPromise using `Unpromise.proxy(promise)`.
*
* The `ProxyPromise<T>` attaches handlers to the original `Promise<T>`
* `.then()` and `.catch()` just once. Promises derived from it use a
* subscription- (and unsubscription-) based mechanism that monitors these
* handlers.
*
* Every time you call `.subscribe()`, `.then()` `.catch()` or `.finally()` on a
* `ProxyPromise<T>` it returns a `SubscribedPromise<T>` having an additional
* `unsubscribe()` method. Calling `unsubscribe()` detaches reference chains
* from the original, potentially long-lived Promise, eliminating memory leaks.
*
* This approach can eliminate the memory leaks that otherwise come about from
* repeated `race()` or `any()` calls invoking `.then()` and `.catch()` multiple
* times on the same long-lived native Promise (subscriptions which can never be
* cleaned up).
*
* `Unpromise.race(promises)` is a reference implementation of `Promise.race`
* avoiding memory leaks when using long-lived unsettled Promises.
*
* `Unpromise.any(promises)` is a reference implementation of `Promise.any`
* avoiding memory leaks when using long-lived unsettled Promises.
*
* `Unpromise.resolve(promise)` returns an ephemeral `SubscribedPromise<T>` for
* any given `Promise<T>` facilitating arbitrary async/await patterns. Behind
* the scenes, `resolve` is implemented simply as
* `Unpromise.proxy(promise).subscribe()`. Don't forget to call `.unsubscribe()`
* to tidy up!
*
*/
export class Unpromise<T> implements ProxyPromise<T> {
/** INSTANCE IMPLEMENTATION */
/** The promise shadowed by this Unpromise<T> */
protected readonly promise: Promise<T> | PromiseLike<T>;
/** Promises expecting eventual settlement (unless unsubscribed first). This list is deleted
* after the original promise settles - no further notifications will be issued. */
protected subscribers: ReadonlyArray<PromiseWithResolvers<T>> | null = [];
/** The Promise's settlement (recorded when it fulfils or rejects). This is consulted when
* calling .subscribe() .then() .catch() .finally() to see if an immediately-resolving Promise
* can be returned, and therefore subscription can be bypassed. */
protected settlement: PromiseSettledResult<T> | null = null;
/** Constructor accepts a normal Promise executor function like `new
* Unpromise((resolve, reject) => {...})` or accepts a pre-existing Promise
* like `new Unpromise(existingPromise)`. Adds `.then()` and `.catch()`
* handlers to the Promise. These handlers pass fulfilment and rejection
* notifications to downstream subscribers and maintains records of value
* or error if the Promise ever settles. */
protected constructor(promise: Promise<T>);
protected constructor(promise: PromiseLike<T>);
protected constructor(executor: PromiseExecutor<T>);
protected constructor(arg: Promise<T> | PromiseLike<T> | PromiseExecutor<T>) {
// handle either a Promise or a Promise executor function
if (typeof arg === "function") {
this.promise = new Promise(arg);
} else {
this.promise = arg;
}
// subscribe for eventual fulfilment and rejection
// handle PromiseLike objects (that at least have .then)
const thenReturn = this.promise.then((value) => {
// atomically record fulfilment and detach subscriber list
const { subscribers } = this;
this.subscribers = null;
this.settlement = {
status: "fulfilled",
value,
};
// notify fulfilment to subscriber list
subscribers?.forEach(({ resolve }) => {
resolve(value);
});
});
// handle Promise (that also have a .catch behaviour)
if ("catch" in thenReturn) {
thenReturn.catch((reason) => {
// atomically record rejection and detach subscriber list
const { subscribers } = this;
this.subscribers = null;
this.settlement = {
status: "rejected",
reason,
};
// notify rejection to subscriber list
subscribers?.forEach(({ reject }) => {
reject(reason);
});
});
}
}
/** Create a promise that mitigates uncontrolled subscription to a long-lived
* Promise via .then() and .catch() - otherwise a source of memory leaks.
*
* The returned promise has an `unsubscribe()` method which can be called when
* the Promise is no longer being tracked by application logic, and which
* ensures that there is no reference chain from the original promise to the
* new one, and therefore no memory leak.
*
* If original promise has not yet settled, this adds a new unique promise
* that listens to then/catch events, along with an `unsubscribe()` method to
* detach it.
*
* If original promise has settled, then creates a new Promise.resolve() or
* Promise.reject() and provided unsubscribe is a noop.
*
* If you call `unsubscribe()` before the returned Promise has settled, it
* will never settle.
*/
subscribe(): SubscribedPromise<T> {
// in all cases we will combine some promise with its unsubscribe function
let promise: Promise<T>;
let unsubscribe: () => void;
const { settlement } = this;
if (settlement === null) {
// not yet settled - subscribe new promise. Expect eventual settlement
if (this.subscribers === null) {
// invariant - it is not settled, so it must have subscribers
throw new Error("Unpromise settled but still has subscribers");
}
const subscriber = withResolvers<T>();
this.subscribers = listWithMember(this.subscribers, subscriber);
promise = subscriber.promise;
unsubscribe = () => {
if (this.subscribers !== null) {
this.subscribers = listWithoutMember(this.subscribers, subscriber);
}
};
} else {
// settled - don't create subscribed promise. Just resolve or reject
const { status } = settlement;
if (status === "fulfilled") {
promise = Promise.resolve(settlement.value);
} else {
promise = Promise.reject(settlement.reason);
}
unsubscribe = NOOP;
}
// extend promise signature with the extra method
return Object.assign(promise, { unsubscribe });
}
/** STANDARD PROMISE METHODS (but returning a SubscribedPromise) */
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| null
,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| null
): SubscribedPromise<TResult1 | TResult2> {
const subscribed = this.subscribe();
const { unsubscribe } = subscribed;
return Object.assign(subscribed.then(onfulfilled, onrejected), {
unsubscribe,
});
}
catch<TResult = never>(
onrejected?:
| ((reason: any) => TResult | PromiseLike<TResult>)
| null
): SubscribedPromise<T | TResult> {
const subscribed = this.subscribe();
const { unsubscribe } = subscribed;
return Object.assign(subscribed.catch(onrejected), {
unsubscribe,
});
}
finally(onfinally?: (() => void) | null ): SubscribedPromise<T> {
const subscribed = this.subscribe();
const { unsubscribe } = subscribed;
return Object.assign(subscribed.finally(onfinally), {
unsubscribe,
});
}
/** TOSTRING SUPPORT */
readonly [Symbol.toStringTag] = "Unpromise";
/** Unpromise STATIC METHODS */
/** Create or Retrieve the proxy Unpromise (a re-used Unpromise for the VM lifetime
* of the provided Promise reference) */
static proxy<T>(promise: PromiseLike<T>): ProxyPromise<T> {
const cached = Unpromise.getSubscribablePromise(promise);
return typeof cached !== "undefined"
? cached
: Unpromise.createSubscribablePromise(promise);
}
/** Create and store an Unpromise keyed by an original Promise. */
protected static createSubscribablePromise<T>(promise: PromiseLike<T>) {
const created = new Unpromise<T>(promise);
subscribableCache.set(promise, created as Unpromise<unknown>); // resolve promise to unpromise
subscribableCache.set(created, created as Unpromise<unknown>); // resolve the unpromise to itself
return created;
}
/** Retrieve a previously-created Unpromise keyed by an original Promise. */
protected static getSubscribablePromise<T>(promise: PromiseLike<T>) {
return subscribableCache.get(promise) as ProxyPromise<T> | undefined;
}
/** Promise STATIC METHODS */
/** Lookup the Unpromise for this promise, and derive a SubscribedPromise from
* it (that can be later unsubscribed to eliminate Memory leaks) */
static resolve<T>(value: T | PromiseLike<T>) {
const promise: PromiseLike<T> =
typeof value === "object" &&
value !== null &&
"then" in value &&
typeof value.then === "function"
? value
: Promise.resolve(value);
return Unpromise.proxy(promise).subscribe() as SubscribedPromise<
Awaited<T>
>;
}
/** Perform Promise.any() via SubscribedPromises, then unsubscribe them.
* Equivalent to Promise.any but eliminates memory leaks from long-lived
* promises accumulating .then() and .catch() subscribers. */
static async any<T extends readonly unknown[] | []>(
values: T
): Promise<Awaited<T[number]>>;
static async any<T>(
values: Iterable<T | PromiseLike<T>>
): Promise<Awaited<T>> {
const valuesArray = Array.isArray(values) ? values : [...values];
const subscribedPromises = valuesArray.map(Unpromise.resolve);
try {
return await Promise.any(subscribedPromises);
} finally {
subscribedPromises.forEach(({ unsubscribe }) => {
unsubscribe();
});
}
}
/** Perform Promise.race via SubscribedPromises, then unsubscribe them.
* Equivalent to Promise.race but eliminates memory leaks from long-lived
* promises accumulating .then() and .catch() subscribers. */
static async race<T extends readonly unknown[] | []>(
values: T
): Promise<Awaited<T[number]>>;
static async race<T>(
values: Iterable<T | PromiseLike<T>>
): Promise<Awaited<T>> {
const valuesArray = Array.isArray(values) ? values : [...values];
const subscribedPromises = valuesArray.map(Unpromise.resolve);
try {
return await Promise.race(subscribedPromises);
} finally {
subscribedPromises.forEach(({ unsubscribe }) => {
unsubscribe();
});
}
}
/** Create a race of SubscribedPromises that will fulfil to a single winning
* Promise (in a 1-Tuple). Eliminates memory leaks from long-lived promises
* accumulating .then() and .catch() subscribers. Allows simple logic to
* consume the result, like...
* ```ts
* const [ winner ] = await Unpromise.race([ promiseA, promiseB ]);
* if(winner === promiseB){
* const result = await promiseB;
* // do the thing
* }
* ```
* */
static async raceReferences<TPromise extends Promise<unknown>>(
promises: readonly TPromise[]
) {
// map each promise to an eventual 1-tuple containing itself
const selfPromises = promises.map(resolveSelfTuple);
// now race them. They will fulfil to a readonly [P] or reject.
try {
return await Promise.race(selfPromises);
} finally {
for (const promise of selfPromises) {
// unsubscribe proxy promises when the race is over to mitigate memory leaks
promise.unsubscribe();
}
}
}
}
/** Promises a 1-tuple containing the original promise when it resolves. Allows
* awaiting the eventual Promise ***reference*** (easy to destructure and
* exactly compare with ===). Avoids resolving to the Promise ***value*** (which
* may be ambiguous and therefore hard to identify as the winner of a race).
* You can call unsubscribe on the Promise to mitigate memory leaks.
* */
export function resolveSelfTuple<TPromise extends Promise<unknown>>(
promise: TPromise
): SubscribedPromise<readonly [TPromise]> {
return Unpromise.proxy(promise).then(() => [promise] as const);
}
/** VENDORED (Future) PROMISE UTILITIES */
/** Reference implementation of https://github.com/tc39/proposal-promise-with-resolvers */
function withResolvers<T>(): PromiseWithResolvers<T> {
let resolve!: PromiseWithResolvers<T>["resolve"];
let reject!: PromiseWithResolvers<T>["reject"];
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return {
promise,
resolve,
reject,
};
}
/** IMMUTABLE LIST OPERATIONS */
function listWithMember<T>(arr: readonly T[], member: T): readonly T[] {
return [...arr, member];
}
function listWithoutIndex<T>(arr: readonly T[], index: number) {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
}
function listWithoutMember<T>(arr: readonly T[], member: unknown) {
const index = arr.indexOf(member as T);
if (index !== -1) {
return listWithoutIndex(arr, index);
}
return arr;
}