Skip to content

Commit 0e6145a

Browse files
committed
📝 Add client docs
1 parent 0faa6e5 commit 0e6145a

File tree

3 files changed

+167
-7
lines changed

3 files changed

+167
-7
lines changed

src/test/api/v1/cat.proto

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ message ShareLocationResponse {
2626

2727
service Cat {
2828
// Get cat by name
29-
rpc GetCat (GetCatRequest) returns (cats.Cat) {};
29+
rpc GetCat (GetCatRequest) returns (cats.Cat);
3030
// Watch new cats
31-
rpc WatchCats (WatchCatsRequest) returns (stream cats.Cat) {};
31+
rpc WatchCats (WatchCatsRequest) returns (stream cats.Cat);
32+
// Stream my (being a cat) current location, to see how much I travelled
33+
rpc ShareLocation (stream ShareLocationRequest)
34+
returns (ShareLocationResponse);
3235
// Send food, receive incoming consumers
33-
rpc ShareLocation (stream ShareLocationRequest) returns (ShareLocationResponse) {};
34-
// Send food, receive incoming consumers
35-
rpc FeedCats (stream FeedCatsRequest) returns (stream cats.Cat) {};
36-
}
36+
rpc FeedCats (stream FeedCatsRequest) returns (stream cats.Cat);
37+
}

website/docs/wiki/client.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
title: Client
3+
---
4+
5+
For doing a server abstraction, there was a simple reason: lack of middlewares which are (almost) an essential concept to backend server development. With the client, there are no missing features per se, but the interface is a bit basic, extremely verbose and somewhat painful to work with. ProtoCat's client aims at providing a modern familiar interface, that does not take any power of the underlying layer away and preserves (or improves at some points) type safety.
6+
7+
## Getting started - Unary call
8+
9+
Compare the following implementations that achieve the same goal with native grpc interface for node, and ProtoCat's client abstraction with `createClient`:
10+
11+
1. Initialize client
12+
2. Setup request message and set client metadata
13+
3. Obtain server's response, initial and trailing metadata
14+
15+
```typescript
16+
const client = createClient(CatService, ADDR)
17+
const { status, metadata, response } = await client.getCat((req, metadata) => {
18+
req.setName('Meow')
19+
metadata.set('authorization', 'cat-permit')
20+
})
21+
```
22+
23+
```typescript
24+
const client = new CatService(ADDR, ChannelCredentials.createInsecure())
25+
let metadata: Promise<Metadata> = null as any
26+
let status: Promise<StatusObject> = null as any
27+
const clientMeta = new Metadata()
28+
clientMeta.set('authorization', 'cat-permit')
29+
const hello = await new Promise<GetCatResponse>((resolve, reject) => {
30+
const call = client.getCat(
31+
new GetCatRequest().setName('Meow'),
32+
clientMeta,
33+
(err, res) => (err ? reject(err) : resolve(res))
34+
)
35+
metadata = new Promise(resolve => call.on('metadata', resolve))
36+
status = new Promise(resolve => call.on('status', resolve))
37+
})
38+
```
39+
40+
## Call types
41+
42+
While the ProtoCat's client really shines on unary calls, it does support all gRPC call types. Following the premise of keeping the power of underlying implementation, we must tamper with the stream API.
43+
44+
### Server stream
45+
46+
```typescript
47+
const { status, metadata, call } = client.watchCats(req =>
48+
req.onlyWithPointyEars(true)
49+
)
50+
const acc: string[] = []
51+
call.on('data', res => acc.push(res.getName()))
52+
await new Promise(resolve => call.on('end', resolve))
53+
```
54+
55+
### Client stream
56+
57+
```typescript
58+
const { status, metadata, call, response } = client.shareLocation()
59+
'meeoaw!'.split('').forEach(c => {
60+
call.write(
61+
new ShareLocationRequest()
62+
.setLng(c.charCodeAt())
63+
.setLat(Math.random() * c.charCodeAt())
64+
)
65+
})
66+
call.end()
67+
await response
68+
```
69+
70+
### Bidi
71+
72+
```typescript
73+
const { status, metadata, call } = await client.feedCats()
74+
const acc: string[] = []
75+
call.on('data', res => acc.push(res.getName()))
76+
;['lasagne', 'cake', 'fish'].forEach(dish => {
77+
call.write(new FeedCatsRequest().setFood(dish))
78+
})
79+
call.end()
80+
await new Promise(resolve => call.on('end', resolve))
81+
```
82+
83+
## Client initialization
84+
85+
`createClient` accepts the same arguments as the native client, with additional first argument being a _client definition_:
86+
87+
1. Address is mandatory
88+
2. Credentials are mandatory on the underlying implementation, when not supplied insecure channel credentials are provided
89+
3. Client options
90+
91+
The helper creates an instance in a closure and provides stub with the updated API.
92+
93+
Each call instead of getting arguments for `request` (some types), `metadata` and `options`, is provided a setup function, in which user can set the prepared objects.
94+
95+
The client definition is either a client class, or object of client classes:
96+
97+
```typescript
98+
const client = createClient({ cat: CatService, dog: DogService }, ADDR)
99+
await client.cat.getCat()
100+
await client.dog.getDog()
101+
```
102+
103+
This way you can have a single client to access multiple services of a single API, with sharing the connection configuration. In this case, there are several client instances created under the hood with the same configuration and the types are joyfully inferred from the definition!
104+
105+
## Interceptors
106+
107+
Exciting feature of gRPC clients are interceptors. They are like middlewares for clients, allowing you to add hooks for your client actions. It's a powerful concept that allows for uniform caching, logging or retry mechanisms.
108+
109+
The native API is as always basic, verbose and powerful. For many simple use-cases too overwhelming. But since ProtoCat aims to support potentially existing intereceptors and yet provide an elegant way to define custom ones, it proves some basic creators to handle the basic use cases.
110+
111+
:::tip
112+
Need more? You can create your custom interceptor that ones just the thing you need. See [gRFC for NodeJS Client Interceptors](https://github.com/grpc/proposal/blob/master/L5-node-client-interceptors.md) that has detailed overview of the specs the implementation follows.
113+
:::
114+
115+
### Access log interceptor
116+
117+
Middleware-like interface for convenient logging
118+
119+
```typescript
120+
const client = createClient(
121+
CatService,
122+
ADDR,
123+
ChannelCredentials.createInsecure(),
124+
{
125+
interceptors: [
126+
accessLogInterceptor(async (ctx, next) => {
127+
console.log(`${ctx.options.method_definition.path} -->`)
128+
const st = await next()
129+
console.log(`${ctx.options.method_definition.path} <-- (${st.details})`)
130+
}),
131+
],
132+
}
133+
)
134+
```
135+
136+
### Metadata interceptor
137+
138+
If you are required to set client metadata on each request (for example to authenticate), you can let this interceptor take care of that
139+
140+
```typescript
141+
const client = createClient(
142+
CatService,
143+
ADDR,
144+
ChannelCredentials.createInsecure(),
145+
{
146+
interceptors: [
147+
metadataInterceptor(async (meta, opts) => {
148+
const bearer = getTokenForPath(opts.method_definition.path)
149+
meta.set('authorization', `Bearer ${bearer}`)
150+
}),
151+
],
152+
}
153+
)
154+
```

website/sidebars.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ module.exports = {
77
'wiki/starting-server',
88
'wiki/basic-middleware',
99
],
10-
Features: ['wiki/middlewares', 'wiki/metadata', 'wiki/error-handling'],
10+
Features: [
11+
'wiki/middlewares',
12+
'wiki/metadata',
13+
'wiki/error-handling',
14+
'wiki/client',
15+
],
1116
},
1217
}

0 commit comments

Comments
 (0)