Last active
January 2, 2025 22:15
-
-
Save JonathanTurnock/a382107e84c048cade55f921ceff50e1 to your computer and use it in GitHub Desktop.
Miminal Typescript RPC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { RpcRouter } from "./rpc.ts" | |
export type RpcAPI = { | |
foo: typeof fooProvider; | |
}; | |
export const router = new RpcRouter<RpcAPI>({ | |
foo: fooProvider, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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