import { Buffer } from "node:buffer"; import { decode, encode } from "@msgpack/msgpack"; import { isErrorLike } from "./lib/isErrorLike"; import { isR19ErrorLike } from "./lib/isR19ErrorLike"; import { replaceLeaves } from "./lib/replaceLeaves"; import { Procedure, Procedures, ProcedureCallServerArgs, OnErrorEventHandler, } from "./types"; import { R19Error } from "./R19Error"; const findProcedure = ( procedures: Procedures, path: string[], // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Procedure | undefined => { // Use a clone to prevent unwanted mutations. path = [...path]; // eslint-disable-next-line @typescript-eslint/no-explicit-any let proceduresPointer: Procedures | Procedure = procedures; while (path.length > 0) { const pathSegment = path.shift(); if (pathSegment) { proceduresPointer = proceduresPointer[pathSegment]; if (typeof proceduresPointer === "function") { return proceduresPointer; } else if (proceduresPointer === undefined) { return; } } } }; type HandleRPCRequestArgs = { procedures: TProcedures; body: ArrayBuffer | Buffer | undefined; onError?: OnErrorEventHandler; }; type HandleRPCRequestReturnType = { body: Uint8Array; headers: Record; statusCode?: number; }; export const handleRPCRequest = async ( args: HandleRPCRequestArgs, ): Promise => { if (!args.body) { throw new Error( "Invalid request body. Only requests from an r19 client are accepted.", ); } const clientArgs = decode(Buffer.from(args.body)) as ProcedureCallServerArgs; const procedure = findProcedure(args.procedures, clientArgs.procedurePath); const headers = { "Content-Type": "application/msgpack", }; if (!procedure) { const error = new R19Error( `Invalid procedure name: ${clientArgs.procedurePath.join(".")}`, { procedurePath: clientArgs.procedurePath, procedureArgs: clientArgs.procedureArgs, }, ); const body = encode( { error, }, { ignoreUndefined: true }, ); args.onError?.({ error, ...clientArgs }); return { body, headers, statusCode: 500, }; } let res: unknown; try { const procedureArgs = await replaceLeaves( clientArgs.procedureArgs, async (value) => { if (value instanceof ArrayBuffer) { return Buffer.from(value); } return value; }, ); res = await procedure(...procedureArgs); res = await replaceLeaves(res, async (value) => { if (isErrorLike(value)) { return { name: value.name, message: value.message, stack: process.env.NODE_ENV === "development" ? value.stack : undefined, }; } if (typeof value === "function") { throw new R19Error("r19 does not support function return values.", { procedurePath: clientArgs.procedurePath, procedureArgs: clientArgs.procedureArgs, }); } return value; }); } catch (error) { if (isErrorLike(error)) { const body = encode( { error: isR19ErrorLike(error) ? error : { name: error.name, message: error.message, stack: process.env.NODE_ENV === "development" ? error.stack : undefined, }, }, { ignoreUndefined: true }, ); args.onError?.({ error, ...clientArgs }); return { body, headers, statusCode: 500, }; } throw error; } try { const body = encode( { data: res, }, { ignoreUndefined: true }, ); return { body, headers, }; } catch (error) { if (error instanceof Error) { const rpcError = new R19Error( "Unable to serialize server response. Check the server log for details.", { procedurePath: clientArgs.procedurePath, procedureArgs: clientArgs.procedureArgs, cause: error, }, ); console.error(rpcError); const body = encode(rpcError); args.onError?.({ error, ...clientArgs }); return { body, headers, statusCode: 500, }; } throw error; } };