@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
JavaScript
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 };