r19
Version:
Simple remote procedure calls in TypeScript
199 lines (165 loc) • 4.11 kB
text/typescript
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<any> | undefined => {
// Use a clone to prevent unwanted mutations.
path = [...path];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let proceduresPointer: Procedures | Procedure<any> = 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<TProcedures extends Procedures> = {
procedures: TProcedures;
body: ArrayBuffer | Buffer | undefined;
onError?: OnErrorEventHandler;
};
type HandleRPCRequestReturnType = {
body: Uint8Array;
headers: Record<string, string>;
statusCode?: number;
};
export const handleRPCRequest = async <TProcedures extends Procedures>(
args: HandleRPCRequestArgs<TProcedures>,
): Promise<HandleRPCRequestReturnType> => {
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;
}
};