Skip to content

Commit

Permalink
feat: support multple procedure arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
angeloashmore committed Dec 11, 2023
1 parent d3b7b49 commit 289b8f4
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 123 deletions.
4 changes: 2 additions & 2 deletions src/R19Error.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
type R19ErrorOptions = {
procedurePath?: string[];
procedureArgs?: Record<string, unknown>;
procedureArgs?: unknown[];
cause?: unknown;
};

export class R19Error extends Error {
procedurePath?: string[];
procedureArgs?: Record<string, unknown>;
procedureArgs?: unknown[];

constructor(message: string, options: R19ErrorOptions = {}) {
super();
Expand Down
163 changes: 79 additions & 84 deletions src/client/createRPCClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { encode, decode } from "@msgpack/msgpack";

import { isErrorLike } from "../lib/isErrorLike";
import { isPlainObject } from "../lib/isPlainObject";
import { isR19ErrorLike } from "../lib/isR19ErrorLike";
import { replaceLeaves } from "../lib/replaceLeaves";

Expand Down Expand Up @@ -41,20 +40,26 @@ type TransformProcedures<TProcedures> =

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TransformProcedure<TProcedure extends Procedure<any>> = (
...args: Parameters<TProcedure> extends []
? []
: [TransformProcedureArgs<Parameters<TProcedure>[0]>]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: TransformProcedureArgs<Parameters<TProcedure>> extends any[]
? TransformProcedureArgs<Parameters<TProcedure>>
: []
) => Promise<TransformProcedureReturnType<Awaited<ReturnType<TProcedure>>>>;

type TransformProcedureArgs<TArgs> = TArgs extends
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TransformProcedureArgs<TArgs extends any[]> = {
[P in keyof TArgs]: TransformProcedureArg<TArgs[P]>;
};

type TransformProcedureArg<TArg> = TArg extends
| Record<string, unknown>
| unknown[]
? {
[P in keyof TArgs]: TransformProcedureArgs<TArgs[P]>;
[P in keyof TArg]: TransformProcedureArg<TArg[P]>;
}
: TArgs extends Buffer
: TArg extends Buffer
? Blob
: TArgs;
: TArg;

type TransformProcedureReturnType<TReturnType> = TReturnType extends
| Record<string, unknown>
Expand Down Expand Up @@ -95,93 +100,83 @@ export const createRPCClient = <TProcedures extends Procedures>(
const resolvedFetch: FetchLike =
args.fetch || globalThis.fetch.bind(globalThis);

return createArbitrarilyNestedFunction(async (procedurePath, fnArgs) => {
const procedureArgs = fnArgs[0] as Record<string, unknown>;
return createArbitrarilyNestedFunction(
async (procedurePath, procedureArgs) => {
const preparedProcedureArgs = await replaceLeaves(
procedureArgs,
async (value) => {
if (value instanceof Blob) {
return new Uint8Array(await value.arrayBuffer());
}

if (typeof value === "function") {
throw new R19Error("r19 does not support function arguments.", {
procedurePath,
procedureArgs,
});
}

return value;
},
);

if (procedureArgs !== undefined && !isPlainObject(procedureArgs)) {
throw new R19Error(
"r19 only supports a single object procedure argument, but something else was provided.",
const body = encode(
{
procedurePath,
procedureArgs,
procedurePath: procedurePath,
procedureArgs: preparedProcedureArgs,
},
{ ignoreUndefined: true },
);
}

const preparedProcedureArgs = await replaceLeaves(
procedureArgs,
async (value) => {
if (value instanceof Blob) {
return new Uint8Array(await value.arrayBuffer());
}
const res = await resolvedFetch(args.serverURL, {
method: "POST",
body,
headers: {
"Content-Type": "application/msgpack",
},
});

const arrayBuffer = await res.arrayBuffer();
const resObject = decode(
new Uint8Array(arrayBuffer),
) as ProcedureCallServerResponse;

if ("error" in resObject) {
const resError = resObject.error;

if (typeof value === "function") {
throw new R19Error("r19 does not support function arguments.", {
if (isR19ErrorLike(resError)) {
const error = new R19Error(resError.message, {
procedurePath,
procedureArgs,
});
error.stack = resError.stack;

throw error;
} else if (isErrorLike(resError)) {
const error = new Error(resError.message);
error.name = resError.name;
error.stack = resError.stack;

throw error;
} else {
throw new R19Error(
"An unexpected response was received from the RPC server.",
{
procedurePath,
procedureArgs,
cause: resObject,
},
);
}
} else {
return replaceLeaves(resObject.data, async (value) => {
if (value instanceof Uint8Array) {
return new Blob([value]);
}

return value;
},
);

const body = encode(
{
procedurePath: procedurePath,
procedureArgs: preparedProcedureArgs,
},
{ ignoreUndefined: true },
);

const res = await resolvedFetch(args.serverURL, {
method: "POST",
body,
headers: {
"Content-Type": "application/msgpack",
},
});

const arrayBuffer = await res.arrayBuffer();
const resObject = decode(
new Uint8Array(arrayBuffer),
) as ProcedureCallServerResponse;

if ("error" in resObject) {
const resError = resObject.error;

if (isR19ErrorLike(resError)) {
const error = new R19Error(resError.message, {
procedurePath,
procedureArgs,
return value;
});
error.stack = resError.stack;

throw error;
} else if (isErrorLike(resError)) {
const error = new Error(resError.message);
error.name = resError.name;
error.stack = resError.stack;

throw error;
} else {
throw new R19Error(
"An unexpected response was received from the RPC server.",
{
procedurePath,
procedureArgs,
cause: resObject,
},
);
}
} else {
return replaceLeaves(resObject.data, async (value) => {
if (value instanceof Uint8Array) {
return new Blob([value]);
}

return value;
});
}
});
},
);
};
2 changes: 1 addition & 1 deletion src/handleRPCRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const handleRPCRequest = async <TProcedures extends Procedures>(
},
);

res = await procedure(procedureArgs);
res = await procedure(...procedureArgs);

res = await replaceLeaves(res, async (value) => {
if (isErrorLike(value)) {
Expand Down
17 changes: 0 additions & 17 deletions src/lib/isPlainObject.ts

This file was deleted.

21 changes: 15 additions & 6 deletions src/lib/replaceLeaves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,27 @@ const isPlainObject = <Value>(

type Replacer = (value: unknown) => unknown | Promise<unknown>;

export const replaceLeaves = async (
input: unknown,
type ReplaceLeavesReturnValue<TInput> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TInput extends any[]
? unknown[]
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
TInput extends Record<PropertyKey, any>
? { [P in keyof TInput]: unknown }
: unknown;

export const replaceLeaves = async <TInput>(
input: TInput,
replacer: Replacer,
): Promise<unknown> => {
): Promise<ReplaceLeavesReturnValue<TInput>> => {
if (Array.isArray(input)) {
const preparedProcedureArgs: unknown[] = [];

for (let i = 0; i < input.length; i++) {
preparedProcedureArgs[i] = await replaceLeaves(input[i], replacer);
}

return preparedProcedureArgs;
return preparedProcedureArgs as ReplaceLeavesReturnValue<TInput>;
}

if (isPlainObject(input)) {
Expand All @@ -42,8 +51,8 @@ export const replaceLeaves = async (
);
}

return preparedProcedureArgs;
return preparedProcedureArgs as ReplaceLeavesReturnValue<TInput>;
}

return await replacer(input);
return (await replacer(input)) as ReplaceLeavesReturnValue<TInput>;
};
2 changes: 1 addition & 1 deletion src/proceduresFromInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export type OmittableProcedures<TProceduresInstance> = AllObjDotPaths<

export type ProceduresFromInstance<
TProceduresInstance,
TOmitPaths extends string,
TOmitPaths extends string = never,
> = RecursiveOmitNested<OnlyProcedures<TProceduresInstance>, TOmitPaths>;

type ProceduresFromInstanceConfig<
Expand Down
7 changes: 4 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ export type Procedures = Record<
>
>;

export type Procedure<TArgs extends Record<string, unknown>> = (
args: TArgs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Procedure<TArgs extends any[]> = (
...args: TArgs
) => unknown | Promise<unknown>;

export type ProcedureCallServerArgs = {
procedurePath: string[];
procedureArgs: Record<string, unknown>;
procedureArgs: unknown[];
};

export type ProcedureCallServerResponse =
Expand Down
16 changes: 7 additions & 9 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ it("does not support function arguments", async () => {
name: "R19Error",
message: expect.stringMatching(/does not support function arguments/i),
procedurePath: ["ping"],
procedureArgs: { fn: fnArg },
procedureArgs: [{ fn: fnArg }],
}),
);

Expand All @@ -225,7 +225,7 @@ it("does not support function return values", async () => {
name: "R19Error",
message: expect.stringMatching(/does not support function return values/i),
procedurePath: ["ping"],
procedureArgs: undefined,
procedureArgs: [],
});

await expect(async () => {
Expand All @@ -237,7 +237,7 @@ it("does not support function return values", async () => {
expect(onError).toHaveBeenCalledWith({
error: expectedError,
procedurePath: ["ping"],
procedureArgs: undefined,
procedureArgs: [],
});
});

Expand Down Expand Up @@ -272,7 +272,7 @@ it("does not support class arguments", async () => {
expect(onError).toHaveBeenCalledWith({
error: expectedError,
procedurePath: ["ping"],
procedureArgs: { foo: fooArg },
procedureArgs: [{ foo: fooArg }],
});
});

Expand Down Expand Up @@ -324,9 +324,7 @@ it("supports `onError` event handler", async () => {
name: "Error",
message: "foo",
}),
procedureArgs: {
input: "foo",
},
procedureArgs: [{ input: "foo" }],
procedurePath: ["throw"],
});
});
Expand Down Expand Up @@ -363,7 +361,7 @@ it("throws if a non-existent procedure is called", async () => {
name: "R19Error",
message: expect.stringMatching(/invalid procedure name: pong/i),
procedurePath: ["pong"],
procedureArgs: { input: "foo" },
procedureArgs: [{ input: "foo" }],
});

await expect(async () => {
Expand All @@ -376,6 +374,6 @@ it("throws if a non-existent procedure is called", async () => {
expect(onError).toHaveBeenCalledWith({
error: expectedError,
procedurePath: ["pong"],
procedureArgs: { input: "foo" },
procedureArgs: [{ input: "foo" }],
});
});

0 comments on commit 289b8f4

Please sign in to comment.