Skip to content

Instantly share code, notes, and snippets.

@JonathanTurnock
Last active January 2, 2025 22:15
Show Gist options
  • Save JonathanTurnock/a382107e84c048cade55f921ceff50e1 to your computer and use it in GitHub Desktop.
Save JonathanTurnock/a382107e84c048cade55f921ceff50e1 to your computer and use it in GitHub Desktop.
Miminal Typescript RPC
import { RpcClient } from "./rpc.ts"
import type { RpcAPI } from "./router.ts"
export const client = new RpcClient<RpcAPI>();
const foo = rpcClient.get("foo");
foo.getFoo().then(console.log)
let foo = "Foo";
function getFoo(): string {
return foo;
}
function setFoo(_foo: string): string {
console.log(`Invoked Set Foo with ${_foo}`);
foo = _foo;
return foo;
}
export const fooProvider = {
getFoo,
setFoo,
};
import { RpcRouter } from "./rpc.ts"
export type RpcAPI = {
foo: typeof fooProvider;
};
export const router = new RpcRouter<RpcAPI>({
foo: fooProvider,
});
export type RemoteProcedure = (...args: any[]) => any;
export type RemoteProcedureProvider = Record<string, RemoteProcedure>;
export type RemoteProcedureProviders = Record<string, RemoteProcedureProvider>;
export type AsPromises<T> = {
[P in keyof T]: T[P] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: never;
};
export class JsonResponse extends Response {
constructor(data: any, init?: ResponseInit) {
super(JSON.stringify(data), {
headers: { ...init?.headers, "Content-Type": "application/json" },
...init,
});
}
}
export class RpcRouter<
PROVIDERS extends RemoteProcedureProviders,
> {
constructor(private readonly providers: PROVIDERS) {
}
async handle(request: Request): Promise<Response> {
const { provider, method, args } = await request.json();
try {
if (!provider || !method || !args) {
return new JsonResponse({ error: "Invalid request" }, { status: 400 });
}
const providerInstance = this.providers[provider];
if (!providerInstance) {
return new JsonResponse({
error: `Provider with name ${provider} not found`,
}, { status: 400 });
}
if (!providerInstance[method]) {
return new JsonResponse({
error: `Method ${method} not found on provider ${provider}`,
}, { status: 400 });
}
if (typeof providerInstance[method] !== "function") {
return new JsonResponse({
error:
`Method ${method} is not a function on provider ${provider} so it cannot be called`,
}, { status: 500 });
}
const result = await providerInstance[method](...args);
return new JsonResponse(result);
} catch (error) {
return new JsonResponse(
error instanceof Error
? {
name: error.name,
error: error.message,
stack: error.stack,
cause: error.cause,
}
: { error: `unknown ${typeof error} error: ${error}` },
{ status: 500 },
);
}
}
}
export class RpcClient<PROVIDERS extends RemoteProcedureProviders> {
get<NAMESPACE extends keyof PROVIDERS>(
provider: NAMESPACE,
): AsPromises<PROVIDERS[NAMESPACE]> {
return new Proxy(
{},
{
get: (_target, method) => {
return async (...args: any[]) => {
const result = await fetch("/rpc2", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ provider, method, args }),
});
return result.json();
};
},
},
) as AsPromises<PROVIDERS[NAMESPACE]>;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment