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

Installation

1
yarn add express @matechs/express @matechs/rpc @matechs/http-client
2
yarn add -D @types/express
3
4
// one http implementation like
5
yarn add @matechs/http-client-libcurl
Copied!

Module

1
import { effect as T } from "@matechs/effect";
2
import { FunctionN } from "fp-ts/lib/function";
3
import * as H from "@matechs/http-client";
4
import * as E from "@matechs/express";
5
import { array } from "fp-ts/lib/Array";
6
import { right } from "fp-ts/lib/Either";
7
import { Exit } from "@matechs/effect/lib/original/exit";
8
import { Do } from "fp-ts-contrib/lib/Do";
9
import { isSome } from "fp-ts/lib/Option";
10
11
// environment entries
12
const clientConfigEnv: unique symbol = Symbol();
13
const serverConfigEnv: unique symbol = Symbol();
14
15
// describe an environment that supports RPC
16
export type Remote<T> = Record<
17
keyof T,
18
Record<string, FunctionN<any, T.Effect<any, any, any>>>
19
>;
20
21
// describe a client configuration
22
interface ClientConfig<M, K extends keyof M> {
23
[k in K]: {
24
baseUrl: string;
25
};
26
};
27
}
28
29
// create a client configuration
30
export function clientConfig<M, K extends keyof M>(
31
_m: M,
32
k: K
33
): (c: ClientConfig<M, K>[typeof clientConfigEnv][K]) => ClientConfig<M, K>
34
35
// describe a server configuration
36
interface ServerConfig<M, K extends keyof M> {
37
[k in K]: {
38
scope: string;
39
};
40
};
41
}
42
43
// create a server configuration
44
export function serverConfig<M, K extends keyof M>(
45
_m: M,
46
k: K
47
): (c: ServerConfig<M, K>[typeof serverConfigEnv][K]) => ServerConfig<M, K>
48
49
// create a client for module M and entry K
50
export function client<M extends Remote<M>, K extends keyof M>(m: M, k: K): Client<M, K>
51
52
// merge environment requirements for the whole module
53
export type Runtime<M> = M extends {
54
[h: string]: (...args: any[]) => T.Effect<infer Q & E.RequestContext, any, any>;
55
}
56
? Q
57
: never;
58
59
// request object
60
interface RPCRequest {
61
args: unknown[];
62
}
63
64
// response object
65
interface RPCResponse {
66
value: Exit<unknown, unknown>;
67
}
68
69
// bind module M and entry K to express
70
export function bind<M extends Remote<M>, K extends keyof M>(
71
m: M,
72
k: K
73
): T.Effect<
74
E.ExpressEnv & Runtime<M[K]> & ServerConfig<M, K> & M,
75
T.NoErr,
76
void
77
>
Copied!

Usage

shared.ts
1
import { effect as T } from "@matechs/effect";
2
import * as RPC from "@matechs/rpc";
3
import * as H from "@matechs/http-client";
4
import { Option } from "fp-ts/lib/Option";
5
import { pipe } from "fp-ts/lib/pipeable";
6
7
// environment entries
8
export const placeholderJsonEnv: unique symbol = Symbol();
9
10
// simple todo interface
11
export interface Todo {
12
userId: number;
13
id: number;
14
title: string;
15
completed: boolean;
16
}
17
18
// describe the service we want to expose
19
export interface PlaceholderJson extends RPC.Remote<PlaceholderJson> {
20
getTodo: (n: number) => T.Effect<H.RequestEnv, string, Option<Todo>>;
21
};
22
}
23
24
/*
25
* Note this is the real implementation, but it doesn't depend
26
* on any library directly only through Env
27
* in this case this can be shared between server and client
28
* if you want to move this to server code only
29
* you can create a dumb implementation like below
30
* that is considered enough for client generation
31
*/
32
33
// implement the service
34
export const placeholderJsonLive: PlaceholderJson = {
35
getTodo: n =>
36
pipe(
37
H.get<unknown, Todo>(`https://jsonplaceholder.typicode.com/todos/${n}`),
38
T.chainError(() => T.raiseError("error fetching todo")),
39
T.map(({ body }) => body)
40
)
41
}
42
};
43
44
// spec for client generation
45
// used if placeholderJsonLive -> server.ts
46
export const placeholderJsonSpec: PlaceholderJson = {
47
getTodo: {} as any
48
}
49
};
Copied!
server.ts
1
import { effect as T, exit as E } from "@matechs/effect";
2
import * as RPC from "@matechs/rpc";
3
import * as H from "@matechs/http-client";
4
import * as EX from "@matechs/express";
5
import * as L from "@matechs/http-client-libcurl";
6
import { pipe } from "fp-ts/lib/pipeable";
7
import { Do } from "fp-ts-contrib/lib/Do";
8
import { placeholderJsonLive, placeholderJsonEnv } from "./shared";
9
10
// create a new express server
11
const program = EX.withApp(
12
Do(T.effect)
13
// wire module to express
14
.do(RPC.bind(placeholderJsonLive, placeholderJsonEnv))
15
// listen on port 8081
16
.bind("server", EX.bind(8081))
17
// return node server
18
.return(s => s.server)
19
);
20
21
// construct live environment
22
const envLive = pipe(
23
T.noEnv,
24
T.mergeEnv(EX.express),
25
T.mergeEnv(L.libcurl()),
26
T.mergeEnv(H.jsonDeserializer),
27
T.mergeEnv(placeholderJsonLive),
28
// configure RPC server for module <placeholderJsonLive, placeholderJsonEnv>
29
T.mergeEnv(
30
RPC.serverConfig(
31
placeholderJsonLive,
32
placeholderJsonEnv
33
)({
34
scope: "/placeholderJson" // exposed at /placeholderJson
35
})
36
)
37
);
38
39
// run express server
40
T.run(
41
T.provideAll(envLive)(program),
42
E.fold(
43
server => {
44
// listen for exit Ctrl+C
45
process.on("SIGINT", () => {
46
server.close(err => {
47
process.exit(err ? 2 : 0);
48
});
49
});
50
51
// listen for SIGTERM
52
process.on("SIGTERM", () => {
53
server.close(err => {
54
process.exit(err ? 2 : 0);
55
});
56
});
57
},
58
e => console.error(e),
59
e => console.error(e),
60
() => console.error("interrupted")
61
)
62
);
Copied!
client.ts
1
import { effect as T, exit as E } from "@matechs/effect";
2
import * as RPC from "@matechs/rpc";
3
import * as H from "@matechs/http-client";
4
import * as L from "@matechs/http-client-libcurl";
5
import { pipe } from "fp-ts/lib/pipeable";
6
import { placeholderJsonLive, placeholderJsonEnv } from "./shared";
7
import * as A from "fp-ts/lib/Array";
8
9
const { getTodo } = RPC.client(placeholderJsonLive, placeholderJsonEnv);
10
11
const program = A.array.traverse(T.parEffect)(A.range(1, 10), getTodo);
12
13
const envLive = pipe(
14
T.noEnv,
15
T.mergeEnv(L.libcurl()),
16
T.mergeEnv(H.jsonDeserializer),
17
T.mergeEnv(
18
RPC.clientConfig(
19
placeholderJsonLive,
20
placeholderJsonEnv
21
)({
22
baseUrl: "http://127.0.0.1:8081/placeholderJson"
23
})
24
)
25
);
26
27
// run express server
28
T.run(
29
T.provideAll(envLive)(program),
30
E.fold(
31
todos => {
32
console.log(todos);
33
},
34
e => console.error(e),
35
e => console.error(e),
36
() => console.error("interrupted")
37
)
38
);
Copied!
1
$ yarn ts-node src/server.ts
2
...
Copied!
1
$ yarn ts-node src/client.ts
2
[
3
{
4
_tag: 'Some',
5
value: { userId: 1, id: 1, title: 'delectus aut autem', completed: false }
6
},
7
{
8
_tag: 'Some',
9
value: {
10
userId: 1,
11
id: 2,
12
title: 'quis ut nam facilis et officia qui',
13
completed: false
14
}
15
},
16
{
17
_tag: 'Some',
18
value: {
19
userId: 1,
20
id: 3,
21
title: 'fugiat veniam minus',
22
completed: false
23
}
24
},
25
{
26
_tag: 'Some',
27
value: { userId: 1, id: 4, title: 'et porro tempora', completed: true }
28
},
29
{
30
_tag: 'Some',
31
value: {
32
userId: 1,
33
id: 5,
34
title: 'laboriosam mollitia et enim quasi adipisci quia provident illum',
35
completed: false
36
}
37
},
38
{
39
_tag: 'Some',
40
value: {
41
userId: 1,
42
id: 6,
43
title: 'qui ullam ratione quibusdam voluptatem quia omnis',
44
completed: false
45
}
46
},
47
{
48
_tag: 'Some',
49
value: {
50
userId: 1,
51
id: 7,
52
title: 'illo expedita consequatur quia in',
53
completed: false
54
}
55
},
56
{
57
_tag: 'Some',
58
value: {
59
userId: 1,
60
id: 8,
61
title: 'quo adipisci enim quam ut ab',
62
completed: true
63
}
64
},
65
{
66
_tag: 'Some',
67
value: {
68
userId: 1,
69
id: 9,
70
title: 'molestiae perspiciatis ipsa',
71
completed: false
72
}
73
},
74
{
75
_tag: 'Some',
76
value: {
77
userId: 1,
78
id: 10,
79
title: 'illo est ratione doloremque quia maiores aut',
80
completed: true
81
}
82
}
83
]
Copied!

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.
Last modified 1yr ago