Skip to content

Instantly share code, notes, and snippets.

@mctep
Last active December 5, 2020 18:12
Show Gist options
  • Save mctep/672846e55ae4ff8c84de27589484b442 to your computer and use it in GitHub Desktop.
Save mctep/672846e55ae4ff8c84de27589484b442 to your computer and use it in GitHub Desktop.
PoC Typescript RPC Client & Server
export const backend = {
echo: async (message: string) => message,
};
export type Backend = typeof backend;
import express from 'express';
import { backend } from './01-backend';
import { createService } from './04-create-service';
async function main() {
const port = 4000;
const app = express();
const backendService = createService(backend);
app.post('/api', express.text(), async (req, res) => {
try {
res.send(await backendService(req.body));
} catch {
res.end();
}
});
app.listen(port, () => {
console.log(`🚀 Server ready at: ${String(port)} port`);
});
}
main();
import fetch from 'node-fetch';
import { Backend } from './01-backend';
import { createClient } from './05-create-client';
const client = createClient<Backend>(async (body) => {
const res = await fetch('http://localhost:4000/api', {
method: 'POST',
body,
});
return rest.text();
});
async function main() {
console.log(await client.echo('Hello world'));
}
main();
import { Resolver, IRequest, Json } from './06-protocol';
export function createService<T extends Resolver<T>>(resolver: T) {
return async (body: string): Promise<string> => {
const req: IRequest = JSON.parse(body);
try {
const result = await ((resolver as any)[req.method] as (
...args: Json[]
) => Promise<Json>).apply(resolver, req.args);
return JSON.stringify({ type: 'success', result });
} catch (result) {
return JSON.stringify({ type: 'error', result });
}
};
}
import { IRequest, IResponse, Json, Resolver } from './06-protocol';
type IRequester = (body: string) => Promise<string>;
export function createClient<T extends Resolver<T>>(requester: IRequester): T {
return new Proxy({} as T, {
get(_, method) {
if (typeof method !== 'string') {
throw new Error(`${String(method)} property does not found`);
}
return async (...args: Json[]) => {
const req: IRequest = { method, args };
const res: IResponse = JSON.parse(await requester(JSON.stringify(req)));
if (res.type === 'error') {
throw res.result;
}
return res.result;
};
},
});
}
export type Json =
| null
| boolean
| number
| string
| Json[]
| { [prop: string]: Json };
export type JsonCompatible<T> = {
[P in keyof T]: T[P] extends Json
? T[P]
: Pick<T, P> extends Required<Pick<T, P>>
? never
: T[P] extends (() => any) | undefined
? never
: JsonCompatible<T[P]>;
};
export type JsonCompatibleResolver<T> = T extends (
...args: Array<infer A>
) => Promise<infer R>
? A extends JsonCompatible<A>
? R extends JsonCompatible<R>
? T
: never
: never
: never;
export type Resolver<T> = { [K in keyof T]: JsonCompatibleResolver<T[K]> };
export type IRequest = { method: string; args: Json[] };
export type IResponse =
| { type: 'error'; result: Json }
| { type: 'success'; result: Json };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment