UNPKG

@orpc/server

Version:

<div align="center"> <image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" /> </div>

220 lines (213 loc) • 8.09 kB
import { toHttpPath, StandardRPCJsonSerializer, StandardRPCSerializer } from '@orpc/client/standard'; import { ORPCError, toORPCError } from '@orpc/client'; import { toArray, intercept, runWithSpan, ORPC_NAME, isAsyncIteratorObject, asyncIteratorWithSpan, setSpanError, parseEmptyableJSON, NullProtoObj, value } from '@orpc/shared'; import { flattenHeader } from '@orpc/standard-server'; import { c as createProcedureClient, t as traverseContractProcedures, i as isProcedure, u as unlazy, g as getRouter, a as createContractedProcedure } from './server.Ds4HPpvH.mjs'; class CompositeStandardHandlerPlugin { plugins; constructor(plugins = []) { this.plugins = [...plugins].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); } init(options, router) { for (const plugin of this.plugins) { plugin.init?.(options, router); } } } class StandardHandler { constructor(router, matcher, codec, options) { this.matcher = matcher; this.codec = codec; const plugins = new CompositeStandardHandlerPlugin(options.plugins); plugins.init(options, router); this.interceptors = toArray(options.interceptors); this.clientInterceptors = toArray(options.clientInterceptors); this.rootInterceptors = toArray(options.rootInterceptors); this.matcher.init(router); } interceptors; clientInterceptors; rootInterceptors; async handle(request, options) { const prefix = options.prefix?.replace(/\/$/, "") || void 0; if (prefix && !request.url.pathname.startsWith(`${prefix}/`) && request.url.pathname !== prefix) { return { matched: false, response: void 0 }; } return intercept( this.rootInterceptors, { ...options, request, prefix }, async (interceptorOptions) => { return runWithSpan( { name: `${request.method} ${request.url.pathname}` }, async (span) => { let step; try { return await intercept( this.interceptors, interceptorOptions, async ({ request: request2, context, prefix: prefix2 }) => { const method = request2.method; const url = request2.url; const pathname = prefix2 ? url.pathname.replace(prefix2, "") : url.pathname; const match = await runWithSpan( { name: "find_procedure" }, () => this.matcher.match(method, `/${pathname.replace(/^\/|\/$/g, "")}`) ); if (!match) { return { matched: false, response: void 0 }; } span?.updateName(`${ORPC_NAME}.${match.path.join("/")}`); span?.setAttribute("rpc.system", ORPC_NAME); span?.setAttribute("rpc.method", match.path.join(".")); step = "decode_input"; let input = await runWithSpan( { name: "decode_input" }, () => this.codec.decode(request2, match.params, match.procedure) ); step = void 0; if (isAsyncIteratorObject(input)) { input = asyncIteratorWithSpan( { name: "consume_event_iterator_input", signal: request2.signal }, input ); } const client = createProcedureClient(match.procedure, { context, path: match.path, interceptors: this.clientInterceptors }); step = "call_procedure"; const output = await client(input, { signal: request2.signal, lastEventId: flattenHeader(request2.headers["last-event-id"]) }); step = void 0; const response = this.codec.encode(output, match.procedure); return { matched: true, response }; } ); } catch (e) { if (step !== "call_procedure") { setSpanError(span, e); } const error = step === "decode_input" && !(e instanceof ORPCError) ? new ORPCError("BAD_REQUEST", { message: `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`, cause: e }) : toORPCError(e); const response = this.codec.encodeError(error); return { matched: true, response }; } } ); } ); } } class StandardRPCCodec { constructor(serializer) { this.serializer = serializer; } async decode(request, _params, _procedure) { const serialized = request.method === "GET" ? parseEmptyableJSON(request.url.searchParams.getAll("data").at(-1)) : await request.body(); return this.serializer.deserialize(serialized); } encode(output, _procedure) { return { status: 200, headers: {}, body: this.serializer.serialize(output) }; } encodeError(error) { return { status: error.status, headers: {}, body: this.serializer.serialize(error.toJSON()) }; } } class StandardRPCMatcher { filter; tree = new NullProtoObj(); pendingRouters = []; constructor(options = {}) { this.filter = options.filter ?? true; } init(router, path = []) { const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => { if (!value(this.filter, traverseOptions)) { return; } const { path: path2, contract } = traverseOptions; const httpPath = toHttpPath(path2); if (isProcedure(contract)) { this.tree[httpPath] = { path: path2, contract, procedure: contract, // this mean dev not used contract-first so we can used contract as procedure directly router }; } else { this.tree[httpPath] = { path: path2, contract, procedure: void 0, router }; } }); this.pendingRouters.push(...laziedOptions.map((option) => ({ ...option, httpPathPrefix: toHttpPath(option.path) }))); } async match(_method, pathname) { if (this.pendingRouters.length) { const newPendingRouters = []; for (const pendingRouter of this.pendingRouters) { if (pathname.startsWith(pendingRouter.httpPathPrefix)) { const { default: router } = await unlazy(pendingRouter.router); this.init(router, pendingRouter.path); } else { newPendingRouters.push(pendingRouter); } } this.pendingRouters = newPendingRouters; } const match = this.tree[pathname]; if (!match) { return void 0; } if (!match.procedure) { const { default: maybeProcedure } = await unlazy(getRouter(match.router, match.path)); if (!isProcedure(maybeProcedure)) { throw new Error(` [Contract-First] Missing or invalid implementation for procedure at path: ${toHttpPath(match.path)}. Ensure that the procedure is correctly defined and matches the expected contract. `); } match.procedure = createContractedProcedure(maybeProcedure, match.contract); } return { path: match.path, procedure: match.procedure }; } } class StandardRPCHandler extends StandardHandler { constructor(router, options = {}) { const jsonSerializer = new StandardRPCJsonSerializer(options); const serializer = new StandardRPCSerializer(jsonSerializer); const matcher = new StandardRPCMatcher(options); const codec = new StandardRPCCodec(serializer); super(router, matcher, codec, options); } } export { CompositeStandardHandlerPlugin as C, StandardHandler as S, StandardRPCCodec as a, StandardRPCHandler as b, StandardRPCMatcher as c };