The RPC Module

Describe service interactions using effects, call your server from the browser and forget that API even exists.

Installation

yarn add express @matechs/express @matechs/rpc @matechs/http-client
yarn add -D @types/express
// one http implementation like
yarn add @matechs/http-client-libcurl

Module

import { effect as T } from "@matechs/effect";
import { FunctionN } from "fp-ts/lib/function";
import * as H from "@matechs/http-client";
import * as E from "@matechs/express";
import { array } from "fp-ts/lib/Array";
import { right } from "fp-ts/lib/Either";
import { Exit } from "@matechs/effect/lib/original/exit";
import { Do } from "fp-ts-contrib/lib/Do";
import { isSome } from "fp-ts/lib/Option";
// environment entries
const clientConfigEnv: unique symbol = Symbol();
const serverConfigEnv: unique symbol = Symbol();
// describe an environment that supports RPC
export type Remote<T> = Record<
keyof T,
Record<string, FunctionN<any, T.Effect<any, any, any>>>
>;
// describe a client configuration
interface ClientConfig<M, K extends keyof M> {
[k in K]: {
baseUrl: string;
};
};
}
// create a client configuration
export function clientConfig<M, K extends keyof M>(
_m: M,
k: K
): (c: ClientConfig<M, K>[typeof clientConfigEnv][K]) => ClientConfig<M, K>
// describe a server configuration
interface ServerConfig<M, K extends keyof M> {
[k in K]: {
scope: string;
};
};
}
// create a server configuration
export function serverConfig<M, K extends keyof M>(
_m: M,
k: K
): (c: ServerConfig<M, K>[typeof serverConfigEnv][K]) => ServerConfig<M, K>
// create a client for module M and entry K
export function client<M extends Remote<M>, K extends keyof M>(m: M, k: K): Client<M, K>
// merge environment requirements for the whole module
export type Runtime<M> = M extends {
[h: string]: (...args: any[]) => T.Effect<infer Q & E.RequestContext, any, any>;
}
? Q
: never;
// request object
interface RPCRequest {
args: unknown[];
}
// response object
interface RPCResponse {
value: Exit<unknown, unknown>;
}
// bind module M and entry K to express
export function bind<M extends Remote<M>, K extends keyof M>(
m: M,
k: K
): T.Effect<
E.ExpressEnv & Runtime<M[K]> & ServerConfig<M, K> & M,
T.NoErr,
void
>

Usage

shared.ts
import { effect as T } from "@matechs/effect";
import * as RPC from "@matechs/rpc";
import * as H from "@matechs/http-client";
import { Option } from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/pipeable";
// environment entries
export const placeholderJsonEnv: unique symbol = Symbol();
// simple todo interface
export interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
// describe the service we want to expose
export interface PlaceholderJson extends RPC.Remote<PlaceholderJson> {
getTodo: (n: number) => T.Effect<H.RequestEnv, string, Option<Todo>>;
};
}
/*
* Note this is the real implementation, but it doesn't depend
* on any library directly only through Env
* in this case this can be shared between server and client
* if you want to move this to server code only
* you can create a dumb implementation like below
* that is considered enough for client generation
*/
// implement the service
export const placeholderJsonLive: PlaceholderJson = {
getTodo: n =>
pipe(
H.get<unknown, Todo>(`https://jsonplaceholder.typicode.com/todos/${n}`),
T.chainError(() => T.raiseError("error fetching todo")),
T.map(({ body }) => body)
)
}
};
// spec for client generation
// used if placeholderJsonLive -> server.ts
export const placeholderJsonSpec: PlaceholderJson = {
getTodo: {} as any
}
};
server.ts
import { effect as T, exit as E } from "@matechs/effect";
import * as RPC from "@matechs/rpc";
import * as H from "@matechs/http-client";
import * as EX from "@matechs/express";
import * as L from "@matechs/http-client-libcurl";
import { pipe } from "fp-ts/lib/pipeable";
import { Do } from "fp-ts-contrib/lib/Do";
import { placeholderJsonLive, placeholderJsonEnv } from "./shared";
// create a new express server
const program = EX.withApp(
Do(T.effect)
// wire module to express
.do(RPC.bind(placeholderJsonLive, placeholderJsonEnv))
// listen on port 8081
.bind("server", EX.bind(8081))
// return node server
.return(s => s.server)
);
// construct live environment
const envLive = pipe(
T.noEnv,
T.mergeEnv(EX.express),
T.mergeEnv(L.libcurl()),
T.mergeEnv(H.jsonDeserializer),
T.mergeEnv(placeholderJsonLive),
// configure RPC server for module <placeholderJsonLive, placeholderJsonEnv>
T.mergeEnv(
RPC.serverConfig(
placeholderJsonLive,
placeholderJsonEnv
)({
scope: "/placeholderJson" // exposed at /placeholderJson
})
)
);
// run express server
T.run(
T.provideAll(envLive)(program),
E.fold(
server => {
// listen for exit Ctrl+C
process.on("SIGINT", () => {
server.close(err => {
process.exit(err ? 2 : 0);
});
});
// listen for SIGTERM
process.on("SIGTERM", () => {
server.close(err => {
process.exit(err ? 2 : 0);
});
});
},
e => console.error(e),
e => console.error(e),
() => console.error("interrupted")
)
);
client.ts
import { effect as T, exit as E } from "@matechs/effect";
import * as RPC from "@matechs/rpc";
import * as H from "@matechs/http-client";
import * as L from "@matechs/http-client-libcurl";
import { pipe } from "fp-ts/lib/pipeable";
import { placeholderJsonLive, placeholderJsonEnv } from "./shared";
import * as A from "fp-ts/lib/Array";
const { getTodo } = RPC.client(placeholderJsonLive, placeholderJsonEnv);
const program = A.array.traverse(T.parEffect)(A.range(1, 10), getTodo);
const envLive = pipe(
T.noEnv,
T.mergeEnv(L.libcurl()),
T.mergeEnv(H.jsonDeserializer),
T.mergeEnv(
RPC.clientConfig(
placeholderJsonLive,
placeholderJsonEnv
)({
baseUrl: "http://127.0.0.1:8081/placeholderJson"
})
)
);
// run express server
T.run(
T.provideAll(envLive)(program),
E.fold(
todos => {
console.log(todos);
},
e => console.error(e),
e => console.error(e),
() => console.error("interrupted")
)
);
$ yarn ts-node src/server.ts
...
$ yarn ts-node src/client.ts
[
{
_tag: 'Some',
value: { userId: 1, id: 1, title: 'delectus aut autem', completed: false }
},
{
_tag: 'Some',
value: {
userId: 1,
id: 2,
title: 'quis ut nam facilis et officia qui',
completed: false
}
},
{
_tag: 'Some',
value: {
userId: 1,
id: 3,
title: 'fugiat veniam minus',
completed: false
}
},
{
_tag: 'Some',
value: { userId: 1, id: 4, title: 'et porro tempora', completed: true }
},
{
_tag: 'Some',
value: {
userId: 1,
id: 5,
title: 'laboriosam mollitia et enim quasi adipisci quia provident illum',
completed: false
}
},
{
_tag: 'Some',
value: {
userId: 1,
id: 6,
title: 'qui ullam ratione quibusdam voluptatem quia omnis',
completed: false
}
},
{
_tag: 'Some',
value: {
userId: 1,
id: 7,
title: 'illo expedita consequatur quia in',
completed: false
}
},
{
_tag: 'Some',
value: {
userId: 1,
id: 8,
title: 'quo adipisci enim quam ut ab',
completed: true
}
},
{
_tag: 'Some',
value: {
userId: 1,
id: 9,
title: 'molestiae perspiciatis ipsa',
completed: false
}
},
{
_tag: 'Some',
value: {
userId: 1,
id: 10,
title: 'illo est ratione doloremque quia maiores aut',
completed: true
}
}
]

Notes

You can wire as many modules as you need! Keep in mind both arguments and return types (error/success) must be serializable in order for RPC to do its magic.

Full example with complete separation of server/client and authentication available at https://github.com/mikearnaldi/matechs-effect/tree/master/packages/rpc/demo.