|
| 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 | +``` |
0 commit comments