├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── lib ├── client.js ├── index.js ├── request.js └── response.js ├── package-lock.json ├── package.json ├── readme.hbs └── test ├── basics.test.js ├── defaults.test.js ├── duplex.test.js ├── protos ├── duplex.proto ├── helloworld.proto ├── reqres.proto ├── reqstream.proto └── resstream.proto ├── reqres.test.js ├── reqstream.test.js ├── resstream.test.js ├── retry.test.js └── static ├── helloworld_grpc_pb.js └── helloworld_pb.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | _book 40 | *.orig 41 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | .idea 3 | .DS_Store 4 | node_modules 5 | dump.rdb 6 | npm-debug.log 7 | .nyc_output 8 | test 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "8" 5 | env: 6 | - CXX=g++-4.8 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Bojan Djurkovic 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grpc-caller 2 | 3 | An improved [gRPC](http://www.grpc.io) client. 4 | 5 | [![npm version](https://img.shields.io/npm/v/grpc-caller.svg?style=flat-square)](https://www.npmjs.com/package/grpc-caller) 6 | [![build status](https://img.shields.io/travis/bojand/grpc-caller/master.svg?style=flat-square)](https://travis-ci.org/bojand/grpc-caller) 7 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](https://standardjs.com) 8 | [![License](https://img.shields.io/github/license/bojand/grpc-caller.svg?style=flat-square)](https://raw.githubusercontent.com/bojand/grpc-caller/master/LICENSE) 9 | 10 | #### Features 11 | 12 | * Promisifies request / response (Unary) calls if no callback is supplied 13 | * Promisifies request stream / response calls if no callback is supplied 14 | * Automatically converts plain javascript object to metadata in calls. 15 | * Adds optional retry functionality to request / response (Unary) calls. 16 | * Exposes expanded `Request` API for collecting metadata and status. 17 | 18 | ## Installation 19 | 20 | ``` 21 | $ npm install grpc-caller 22 | ``` 23 | 24 | ## Overview 25 | 26 | #### Improved unary calls 27 | 28 | Works as standard gRPC client: 29 | 30 | ```js 31 | const caller = require('grpc-caller') 32 | const PROTO_PATH = path.resolve(__dirname, './protos/helloworld.proto') 33 | const client = caller('0.0.0.0:50051', PROTO_PATH, 'Greeter') 34 | client.sayHello({ name: 'Bob' }, (err, res) => { 35 | console.log(res) 36 | }) 37 | ``` 38 | 39 | For unary calls, also promisified if callback is not provided: 40 | 41 | ```js 42 | client.sayHello({ name: 'Bob' }) 43 | .then(res => console.log(res)) 44 | ``` 45 | 46 | Which means means you can use is with `async / await` 47 | 48 | ```js 49 | const res = await client.sayHello({ name: 'Bob' }) 50 | console.log(res) 51 | ``` 52 | 53 | For Unary calls we expose `retry` option identical to [async.retry](http://caolan.github.io/async/docs.html#retry). 54 | 55 | ``` 56 | const res = await client.sayHello({ name: 'Bob' }, {}, { retry: 3 }) 57 | console.log(res) 58 | ``` 59 | 60 | #### Improved request stream / response calls 61 | 62 | Lets say we have a remote call `writeStuff` that accepts a stream of messages 63 | and returns some result based on processing of the stream input. 64 | 65 | Works as standard gRPC client: 66 | 67 | ```js 68 | const call = client.writeStuff((err, res) => { 69 | if (err) console.error(err) 70 | console.log(res) 71 | }) 72 | 73 | // ... write stuff to call 74 | ``` 75 | 76 | If no callback is provided we promisify the call such that it returns an **object 77 | with two properties** `call` and `res` such that: 78 | 79 | * `call` - the standard stream to write to as returned normally by grpc 80 | * `res` - a promise that's resolved / rejected when the call is finished, in place of the callback. 81 | 82 | Using destructuring we can do something like: 83 | 84 | ```js 85 | const { call, res } = client.writeStuff() 86 | res 87 | .then(res => console.log(res)) 88 | .catch(err => console.error(err)) 89 | 90 | // ... write stuff to call 91 | ``` 92 | 93 | This means we can abstract the whole operation into a nicer promise returning 94 | async function to use with `async / await` 95 | 96 | ```js 97 | async function writeStuff() { 98 | const { call, res } = client.writeStuff() 99 | // ... write stuff to call 100 | return res 101 | } 102 | 103 | const res = await writeStuff() 104 | console.log(res) 105 | ``` 106 | 107 | #### Automatic `Metadata` creation 108 | 109 | All standard gRPC client calls accept [`Metadata`](http://www.grpc.io/grpc/node/module-src_metadata-Metadata.html) 110 | as first or second parameter (depending on the call type). However one has to 111 | manually create the Metadata object. This module uses 112 | [grpc-create-metadata](https://www.github.com/bojand/grpc-create-metadata) 113 | to automatically create Metadata if plain Javascript object is passed in. 114 | 115 | ```js 116 | // the 2nd parameter will automatically be converted to gRPC Metadata and 117 | // included in the request 118 | const res = await client.sayHello({ name: 'Bob' }, { requestid: 'my-request-id-123' }) 119 | console.log(res) 120 | ``` 121 | 122 | We can still pass an actual `Metadata` object and it will be used as is: 123 | 124 | ```js 125 | const meta = new grpc.Metadata() 126 | meta.add('requestid', 'my-request-id-123') 127 | const res = await client.sayHello({ name: 'Bob' }, meta) 128 | console.log(res) 129 | ``` 130 | 131 | ## Request API 132 | 133 | In addition to simple API above, the library provides a more detailed `"Request"` API that can 134 | be used to control the call details. The API can only be used for Unary and 135 | request streaming calls. 136 | 137 | #### Unary calls 138 | 139 | ```js 140 | const req = new client 141 | .Request('sayHello', { name: 'Bob' }) // call method name and argument 142 | .withMetadata({ requestId: 'bar-123' }) // call request metadata 143 | .withResponseMetadata(true) // we want to collect response metadata 144 | .withResponseStatus(true) // we want to collect the response status 145 | .withRetry(5) // retry options 146 | 147 | const res = await req.exec() 148 | // res is an instance of our `Response` 149 | // we can also call exec() using a callback 150 | 151 | console.log(res.response) // the actual response data { message: 'Hello Bob!' } 152 | console.log(res.metadata) // the response metadata 153 | console.log(res.status) // the response status 154 | console.log(res.call) // the internal gRPC call 155 | ``` 156 | 157 | #### Request streaming calls 158 | 159 | In case of request streaming calls if `exec()` is called with a callback the gRPC `call` stream is returned. 160 | If no callback is provided an object is returned with `call` property being the call stream and `res` 161 | property being a Promise fulfilled when the call is completed. There is no `retry` option for 162 | request streaming calls. 163 | 164 | ```js 165 | 166 | const req = new client.Request('writeStuff') // the call method name 167 | .withMetadata({ requestId: 'bar-123' }) // the call request metadata 168 | .withResponseMetadata(true) // we want to collect response metadata 169 | .withResponseStatus(true) // we want to collect the response status 170 | 171 | const { call, res: resPromise } = req.exec() 172 | 173 | // ... write data to call 174 | 175 | const res = await resPromise // res is our `Response` 176 | 177 | console.log(res.response) // the actual response data 178 | console.log(res.metadata) // the response metadata 179 | console.log(res.status) // the response status 180 | console.log(res.call) // the internal gRPC call 181 | ``` 182 | 183 | ## API Reference 184 | 185 | 186 | 187 | ### Request 188 | A Request class that encapsulates the request of a call. 189 | 190 | **Kind**: global class 191 | 192 | * [Request](#Request) 193 | * [new Request(methodName, param)](#new_Request_new) 194 | * [.withGrpcOptions(opts)](#Request+withGrpcOptions) ⇒ Object 195 | * [.withMetadata(opts)](#Request+withMetadata) ⇒ Object 196 | * [.withRetry(retry)](#Request+withRetry) ⇒ Object 197 | * [.withResponseMetadata(value)](#Request+withResponseMetadata) ⇒ Object 198 | * [.withResponseStatus(value)](#Request+withResponseStatus) ⇒ Object 199 | * [.exec(fn)](#Request+exec) ⇒ Promise \| Object 200 | 201 | 202 | 203 | #### new Request(methodName, param) 204 | Creates a Request instance. 205 | 206 | 207 | | Param | Type | Description | 208 | | --- | --- | --- | 209 | | methodName | String | the method name. | 210 | | param | \* | the call argument in case of `UNARY` calls. | 211 | 212 | 213 | 214 | #### request.withGrpcOptions(opts) ⇒ Object 215 | Create a request with call options. 216 | 217 | **Kind**: instance method of [Request](#Request) 218 | **Returns**: Object - the request instance. 219 | 220 | | Param | Type | Description | 221 | | --- | --- | --- | 222 | | opts | Object | The gRPC call options. | 223 | 224 | 225 | 226 | #### request.withMetadata(opts) ⇒ Object 227 | Create a request with call metadata. 228 | 229 | **Kind**: instance method of [Request](#Request) 230 | **Returns**: Object - the request instance. 231 | 232 | | Param | Type | Description | 233 | | --- | --- | --- | 234 | | opts | Object | The gRPC call metadata. Can either be a plain object or an instance of `grpc.Metadata`. | 235 | 236 | 237 | 238 | #### request.withRetry(retry) ⇒ Object 239 | Create a request with retry options. 240 | 241 | **Kind**: instance method of [Request](#Request) 242 | **Returns**: Object - the request instance. 243 | 244 | | Param | Type | Description | 245 | | --- | --- | --- | 246 | | retry | Number \| Object | The retry options. Identical to `async.retry`. | 247 | 248 | 249 | 250 | #### request.withResponseMetadata(value) ⇒ Object 251 | Create a request indicating whether we want to collect the response metadata. 252 | 253 | **Kind**: instance method of [Request](#Request) 254 | **Returns**: Object - the request instance. 255 | 256 | | Param | Type | Description | 257 | | --- | --- | --- | 258 | | value | Boolean | `true` to collect the response metadata. Default `false`. | 259 | 260 | 261 | 262 | #### request.withResponseStatus(value) ⇒ Object 263 | Create a request indicating whether we want to collect the response status metadata. 264 | 265 | **Kind**: instance method of [Request](#Request) 266 | **Returns**: Object - the request instance. 267 | 268 | | Param | Type | Description | 269 | | --- | --- | --- | 270 | | value | Boolean | `true` to collect the response status metadata. Default `false`. | 271 | 272 | 273 | 274 | #### request.exec(fn) ⇒ Promise \| Object 275 | Execute the request. 276 | 277 | **Kind**: instance method of [Request](#Request) 278 | **Returns**: Promise \| Object - If no callback is provided in case of `UNARY` call a Promise is returned. 279 | If no callback is provided in case of `REQUEST_STREAMING` call an object is 280 | returned with `call` property being the call stream and `res` 281 | property being a Promise fulfilled when the call is completed. 282 | 283 | | Param | Type | Description | 284 | | --- | --- | --- | 285 | | fn | function | Optional callback | 286 | 287 | 288 | 289 | ### Response 290 | A Response class that encapsulates the response of a call using the `Request` API. 291 | 292 | **Kind**: global class 293 | 294 | * [Response](#Response) 295 | * [.call](#Response+call) : Object 296 | * [.response](#Response+response) : Object 297 | * [.metadata](#Response+metadata) : Object 298 | * [.status](#Response+status) : Object 299 | 300 | 301 | 302 | #### response.call : Object 303 | The response's gRPC call. 304 | 305 | **Kind**: instance property of [Response](#Response) 306 | 307 | 308 | #### response.response : Object 309 | The actual response data from the call. 310 | 311 | **Kind**: instance property of [Response](#Response) 312 | 313 | 314 | #### response.metadata : Object 315 | The response metadata. 316 | 317 | **Kind**: instance property of [Response](#Response) 318 | 319 | 320 | #### response.status : Object 321 | The response status metadata. 322 | 323 | **Kind**: instance property of [Response](#Response) 324 | 325 | 326 | ### caller(host, proto, name, credentials, options, defaults) ⇒ Object 327 | Create client isntance. 328 | 329 | **Kind**: global function 330 | 331 | | Param | Type | Description | 332 | | --- | --- | --- | 333 | | host | String | The host to connect to | 334 | | proto | String \| Object | Path to the protocol buffer definition file or Object specifying file to load and load options for proto loader. | 335 | | name | String | In case of proto path the name of the service as defined in the proto definition. | 336 | | credentials | Object | The credentials to use to connect. Defaults to `grpc.credentials.createInsecure()` | 337 | | options | Object | Options to be passed to the gRPC client constructor | 338 | | options.retry | Object | In addition to gRPC client constructor options, we accept a `retry` option. The retry option is identical to `async.retry` and is passed as is to it. This is used only for `UNARY` calls to add automatic retry capability. | 339 | | defaults | Object | Metadata and Options that will be passed to every Request | 340 | 341 | **Example** *(Create client dynamically)* 342 | ```js 343 | const PROTO_PATH = path.resolve(__dirname, './protos/helloworld.proto') 344 | const client = caller('localhost:50051', PROTO_PATH, 'Greeter') 345 | ``` 346 | **Example** *(With options)* 347 | ```js 348 | const file = path.join(__dirname, 'helloworld.proto') 349 | const load = { 350 | // ... proto-loader load options 351 | } 352 | const client = caller('localhost:50051', { file, load }, 'Greeter') 353 | ``` 354 | **Example** *(Create a static client)* 355 | ```js 356 | const services = require('./static/helloworld_grpc_pb') 357 | const client = caller('localhost:50051', services.GreeterClient) 358 | ``` 359 | **Example** *(Pass Options, Default Metadata and Interceptor options)* 360 | ```js 361 | const metadata = { node_id: process.env.CLUSTER_NODE_ID }; 362 | const credentials = grpc.credentials.createInsecure() 363 | const options = { 364 | interceptors = [ bestInterceptorEver ] 365 | } 366 | const client = caller('localhost:50051', PROTO_PATH, 'Greeter', credentials, options, { 367 | metadata: { foo: 'bar' } 368 | }) 369 | 370 | // Now every call with that client will result 371 | // in invoking the interceptor and sending the default metadata 372 | ``` 373 | 374 | * [caller(host, proto, name, credentials, options, defaults)](#caller) ⇒ Object 375 | * [.metadata](#caller.metadata) 376 | * [.wrap](#caller.wrap) 377 | 378 | 379 | 380 | #### caller.metadata 381 | Utility helper function to create Metadata object from plain Javascript object. 382 | See grpc-create-metadata module. 383 | 384 | **Kind**: static property of [caller](#caller) 385 | 386 | 387 | #### caller.wrap 388 | Utility function that can be used to wrap an already constructed client instance. 389 | 390 | **Kind**: static property of [caller](#caller) 391 | ## License 392 | 393 | Apache-2.0 394 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js') 2 | const _ = require('lodash') 3 | const async = require('async') 4 | const create = require('grpc-create-metadata') 5 | const pc = require('promisify-call') 6 | const maybe = require('call-me-maybe') 7 | 8 | const Response = require('./response') 9 | 10 | function createClient (clientProto) { 11 | class GRPCCaller { 12 | constructor (client, metadata = {}, options = {}) { 13 | this.client = client 14 | this.defaults = { metadata, options } 15 | } 16 | 17 | createOptions (options) { 18 | return _.merge({}, this.defaults.options, options) 19 | } 20 | 21 | createMetadata (metadata) { 22 | if (metadata && isGRPCMetadata(metadata)) { 23 | const metadataClone = metadata.clone() 24 | Object.keys(this.defaults.metadata) 25 | .forEach(key => { 26 | if (metadataClone.get(key).length == 0) { 27 | metadataClone.set(key, this.defaults.metadata[key]) 28 | } 29 | }) 30 | 31 | return metadataClone 32 | } 33 | return create(_.merge({}, this.defaults.metadata, metadata)) 34 | } 35 | } 36 | 37 | promisifyClientProto(GRPCCaller.prototype, clientProto) 38 | createExec(GRPCCaller.prototype) 39 | 40 | return GRPCCaller 41 | } 42 | 43 | function promisifyClientProto (targetProto, clientProto) { 44 | // promisify the client 45 | _.forOwn(clientProto, (v, k) => { 46 | if (k === 'constructor') { 47 | return 48 | } 49 | 50 | if (typeof clientProto[k] === 'function') { 51 | if (!v.requestStream && !v.responseStream) { 52 | targetProto[k] = function (arg, metadata, options, fn) { 53 | if (_.isFunction(options)) { 54 | fn = options 55 | options = null 56 | } 57 | 58 | if (_.isFunction(metadata)) { 59 | fn = metadata 60 | metadata = null 61 | options = null 62 | } 63 | 64 | options = this.createOptions(options) 65 | metadata = this.createMetadata(metadata) 66 | 67 | if (_.has(options, 'retry')) { 68 | const retryOpts = options.retry 69 | const callOpts = options ? _.omit(options, 'retry') : options 70 | 71 | if (_.isFunction(fn)) { 72 | async.retry(retryOpts, rCb => { 73 | v.call(this.client, arg, metadata, callOpts, rCb) 74 | }, fn) 75 | } else { 76 | return new Promise((resolve, reject) => { 77 | async.retry(retryOpts, rCb => { 78 | v.call(this.client, arg, metadata, callOpts, rCb) 79 | }, (err, res) => { 80 | if (err) reject(err) 81 | else resolve(res) 82 | }) 83 | }) 84 | } 85 | } else { 86 | const args = _.compact([arg, metadata, options, fn]) 87 | 88 | return pc(this.client, v, ...args) 89 | } 90 | } 91 | } else if (!v.requestStream && v.responseStream) { 92 | targetProto[k] = function (arg, metadata, options) { 93 | options = this.createOptions(options) 94 | metadata = this.createMetadata(metadata) 95 | 96 | const args = _.compact([arg, metadata, options]) 97 | return v.call(this.client, ...args) 98 | } 99 | } else if (v.requestStream && !v.responseStream) { 100 | targetProto[k] = function (metadata, options, fn) { 101 | if (_.isFunction(options)) { 102 | fn = options 103 | options = undefined 104 | } 105 | if (_.isFunction(metadata)) { 106 | fn = metadata 107 | metadata = undefined 108 | } 109 | 110 | options = this.createOptions(options) 111 | metadata = this.createMetadata(metadata) 112 | 113 | if (fn) { // normal call 114 | const args = _.compact([metadata, options, fn]) 115 | return v.call(this.client, ...args) 116 | } else { // dual return promsified call with return { call, res } 117 | const r = {} 118 | const p = new Promise((resolve, reject) => { 119 | const args = _.compact([metadata, options, fn]) 120 | args.push((err, result) => { 121 | if (err) reject(err) 122 | else resolve(result) 123 | }) 124 | r.call = v.call(this.client, ...args) 125 | }) 126 | r.res = p 127 | return r 128 | } 129 | } 130 | } else if (v.requestStream && v.responseStream) { 131 | targetProto[k] = function (metadata, options) { 132 | options = this.createOptions(options) 133 | metadata = this.createMetadata(metadata) 134 | 135 | const args = _.compact([metadata, options]) 136 | return v.call(this.client, ...args) 137 | } 138 | } 139 | } 140 | }) 141 | } 142 | 143 | function createExec (clientProto) { 144 | clientProto.exec = function exec (request, fn) { 145 | const methodName = request.methodName 146 | 147 | if (!_.isFunction(this.client[methodName])) { 148 | throw new Error(`Invalid method: ${methodName}`) 149 | } 150 | 151 | const implFn = this.client[methodName] 152 | 153 | if ((implFn.responseStream && !implFn.requestStream) || 154 | (implFn.responseStream && implFn.requestStream)) { 155 | throw new Error(`Invalid call: ${methodName} cannot be called using Request API`) 156 | } 157 | 158 | const { 159 | param, 160 | retry, 161 | responseMetadata, 162 | responseStatus 163 | } = request 164 | 165 | const options = this.createOptions(request.options) 166 | const metadata = this.createMetadata(request.metadata) 167 | 168 | if (!implFn.responseStream && !implFn.requestStream) { 169 | return maybe(fn, new Promise((resolve, reject) => { 170 | const response = new Response() 171 | let r = 0 172 | if (retry) { 173 | r = retry 174 | } 175 | 176 | async.retry(r, rCb => { 177 | const call = this[methodName](param, metadata, options, rCb) 178 | 179 | response.call = call 180 | 181 | if (responseMetadata) { 182 | call.on('metadata', md => { 183 | response.metadata = md 184 | }) 185 | } 186 | 187 | if (responseStatus) { 188 | call.on('status', status => { 189 | response.status = status 190 | }) 191 | } 192 | }, (err, res) => { 193 | response.response = res 194 | if (err) { 195 | return reject(err) 196 | } 197 | 198 | return resolve(response) 199 | }) 200 | })) 201 | } else if (implFn.requestStream && !implFn.responseStream) { 202 | const r = {} 203 | const response = new Response() 204 | 205 | const { call, res } = this[methodName](metadata, options) 206 | r.call = call 207 | 208 | r.call = call 209 | response.call = call 210 | 211 | if (responseMetadata) { 212 | call.on('metadata', md => { 213 | response.metadata = md 214 | }) 215 | } 216 | 217 | if (responseStatus) { 218 | call.on('status', status => { 219 | response.status = status 220 | }) 221 | } 222 | 223 | r.res = new Promise((resolve, reject) => { 224 | res.then(result => { 225 | response.response = result 226 | resolve(response) 227 | }).catch(e => reject(e)) 228 | }) 229 | 230 | if (fn) { 231 | r.res.then(result => { 232 | fn(null, result) 233 | }).catch(fn) 234 | } 235 | 236 | return fn ? call : r 237 | } else { 238 | throw new Error(`Invalid call: ${methodName} cannot be called using Request API`) 239 | } 240 | } 241 | } 242 | 243 | function isGRPCMetadata (obj) { 244 | if (obj instanceof grpc.Metadata) { 245 | return true 246 | } 247 | const proto = Object.getPrototypeOf(obj) 248 | if (_.isFunction(proto.getMap)) { 249 | return true 250 | } 251 | return false 252 | } 253 | 254 | module.exports = createClient 255 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js') 2 | const _ = require('lodash') 3 | const gi = require('grpc-inspect') 4 | const create = require('grpc-create-metadata') 5 | 6 | const createClient = require('./client') 7 | const BaseRequest = require('./request') 8 | const Response = require('./response') 9 | const protoLoader = require('@grpc/proto-loader') 10 | 11 | module.exports = caller 12 | 13 | /** 14 | * Create client isntance. 15 | * @param {String} host - The host to connect to 16 | * @param {String|Object} proto Path to the protocol buffer definition file or 17 | * Object specifying file to load and load options for proto loader. 18 | * @param {String} name - In case of proto path the name of the service as defined in the proto definition. 19 | * @param {Object} credentials - The credentials to use to connect. Defaults to `grpc.credentials.createInsecure()` 20 | * @param {Object} options - Options to be passed to the gRPC client constructor 21 | * @param {Object} options.retry - In addition to gRPC client constructor options, we accept a `retry` option. 22 | * The retry option is identical to `async.retry` and is passed as is to it. 23 | * This is used only for `UNARY` calls to add automatic retry capability. 24 | * @param {Object} defaults - Metadata and Options that will be passed to every Request 25 | * @returns {Object} 26 | * 27 | * @example Create client dynamically 28 | * const PROTO_PATH = path.resolve(__dirname, './protos/helloworld.proto') 29 | * const client = caller('localhost:50051', PROTO_PATH, 'Greeter') 30 | * 31 | * @example With options 32 | * const file = path.join(__dirname, 'helloworld.proto') 33 | * const load = { 34 | * // ... proto-loader load options 35 | * } 36 | * const client = caller('localhost:50051', { file, load }, 'Greeter') 37 | * 38 | * @example Create a static client 39 | * const services = require('./static/helloworld_grpc_pb') 40 | * const client = caller('localhost:50051', services.GreeterClient) 41 | * 42 | * @example Pass Options, Default Metadata and Interceptor options 43 | * const metadata = { node_id: process.env.CLUSTER_NODE_ID }; 44 | * const credentials = grpc.credentials.createInsecure() 45 | * const options = { 46 | * interceptors = [ bestInterceptorEver ] 47 | * } 48 | * const client = caller('localhost:50051', PROTO_PATH, 'Greeter', credentials, options, { 49 | * metadata: { foo: 'bar' } 50 | * }) 51 | * 52 | * // Now every call with that client will result 53 | * // in invoking the interceptor and sending the default metadata 54 | * 55 | */ 56 | function caller (host, proto, name, credentials, options, defaults) { 57 | let Ctor 58 | if (_.isString(proto) || (_.isObject(proto) && proto.file)) { 59 | let protoFilePath = proto 60 | let loadOptions = {} 61 | 62 | if (_.isObject(proto) && proto.file) { 63 | protoFilePath = proto.file 64 | loadOptions = proto.load || {} 65 | } 66 | 67 | const packageDefinition = protoLoader.loadSync(protoFilePath, loadOptions) 68 | const loaded = grpc.loadPackageDefinition(packageDefinition) 69 | const descriptor = gi(loaded) 70 | if (!descriptor) { 71 | throw new Error('Error parsing protocol buffer') 72 | } 73 | 74 | Ctor = descriptor.client(name) 75 | if (!Ctor) { 76 | throw new Error(`Service name ${name} not found in protocol buffer definition`) 77 | } 78 | } else if (_.isObject(proto)) { 79 | Ctor = proto 80 | options = credentials 81 | defaults = options 82 | credentials = name 83 | } 84 | 85 | const client = new Ctor(host, credentials || grpc.credentials.createInsecure(), options) 86 | const { metadata: defaultMetadata, options: defaultOptions } = Object.assign({}, defaults) 87 | 88 | return wrap(client, defaultMetadata, defaultOptions) 89 | } 90 | 91 | function wrap (client, metadata, options) { 92 | const GRPCCaller = createClient(Object.getPrototypeOf(client)) 93 | 94 | class Request extends BaseRequest { } 95 | 96 | const instance = new GRPCCaller(client, metadata, options) 97 | 98 | instance.Request = Request 99 | instance.Request.prototype.client = instance 100 | 101 | instance.Response = Response 102 | 103 | return instance 104 | } 105 | 106 | /** 107 | * Utility helper function to create Metadata object from plain Javascript object. 108 | * See grpc-create-metadata module. 109 | */ 110 | caller.metadata = create 111 | 112 | /** 113 | * Utility function that can be used to wrap an already constructed client instance. 114 | */ 115 | caller.wrap = wrap 116 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Request class that encapsulates the request of a call. 3 | */ 4 | class Request { 5 | /** 6 | * Creates a Request instance. 7 | * @param {String} methodName the method name. 8 | * @param {*} param the call argument in case of `UNARY` calls. 9 | */ 10 | constructor (methodName, param) { 11 | if (!methodName) { 12 | throw new Error('Request method name required') 13 | } 14 | 15 | this.methodName = methodName 16 | this.param = param 17 | this.responseMetadata = false 18 | this.responseStatus = false 19 | this.retry = null 20 | } 21 | 22 | /** 23 | * Create a request with call options. 24 | * @param {Object} opts The gRPC call options. 25 | * @return {Object} the request instance. 26 | */ 27 | withGrpcOptions (opts) { 28 | this.options = opts 29 | return this 30 | } 31 | 32 | /** 33 | * Create a request with call metadata. 34 | * @param {Object} opts The gRPC call metadata. 35 | * Can either be a plain object or an instance of `grpc.Metadata`. 36 | * @return {Object} the request instance. 37 | */ 38 | withMetadata (metadata) { 39 | this.metadata = metadata 40 | return this 41 | } 42 | 43 | /** 44 | * Create a request with retry options. 45 | * @param {Number | Object} retry The retry options. Identical to `async.retry`. 46 | * @return {Object} the request instance. 47 | */ 48 | withRetry (retry) { 49 | this.retry = retry 50 | return this 51 | } 52 | 53 | /** 54 | * Create a request indicating whether we want to collect the response metadata. 55 | * @param {Boolean} value `true` to collect the response metadata. Default `false`. 56 | * @return {Object} the request instance. 57 | */ 58 | withResponseMetadata (value) { 59 | this.responseMetadata = value 60 | return this 61 | } 62 | 63 | /** 64 | * Create a request indicating whether we want to collect the response status metadata. 65 | * @param {Boolean} value `true` to collect the response status metadata. Default `false`. 66 | * @return {Object} the request instance. 67 | */ 68 | withResponseStatus (value) { 69 | this.responseStatus = value 70 | return this 71 | } 72 | 73 | /** 74 | * Execute the request. 75 | * @param {Function} fn Optional callback 76 | * @return {Promise|Object} If no callback is provided in case of `UNARY` call a Promise is returned. 77 | * If no callback is provided in case of `REQUEST_STREAMING` call an object is 78 | * returned with `call` property being the call stream and `res` 79 | * property being a Promise fulfilled when the call is completed. 80 | */ 81 | exec (fn) { 82 | return this.client.exec(this, fn) 83 | } 84 | } 85 | 86 | module.exports = Request 87 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Response class that encapsulates the response of a call using the `Request` API. 3 | */ 4 | class Response { 5 | constructor (call, response, metadata, status) { 6 | this.call = call 7 | this.response = response 8 | this.metadata = metadata 9 | this.status = status 10 | } 11 | } 12 | 13 | /** 14 | * The response's gRPC call. 15 | * @member {Object} call 16 | * @memberof Response# 17 | */ 18 | 19 | /** 20 | * The actual response data from the call. 21 | * @member {Object} response 22 | * @memberof Response# 23 | */ 24 | 25 | /** 26 | * The response metadata. 27 | * @member {Object} metadata 28 | * @memberof Response# 29 | */ 30 | 31 | /** 32 | * The response status metadata. 33 | * @member {Object} status 34 | * @memberof Response# 35 | */ 36 | 37 | module.exports = Response 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grpc-caller", 3 | "version": "0.14.0", 4 | "description": "An improved Node.js gRPC client", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "ava -v", 8 | "docs": "jsdoc2md lib/*.js --heading-depth 3 --template readme.hbs > README.md" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/bojand/grpc-caller.git" 13 | }, 14 | "author": { 15 | "name": "Bojan D.", 16 | "email": "dbojan@gmail.com" 17 | }, 18 | "license": "Apache-2.0", 19 | "bugs": { 20 | "url": "https://github.com/bojand/grpc-caller/issues" 21 | }, 22 | "homepage": "https://github.com/bojand/grpc-caller", 23 | "keywords": [ 24 | "protocol buffer", 25 | "protobuf", 26 | "grpc", 27 | "client" 28 | ], 29 | "dependencies": { 30 | "@grpc/proto-loader": "^0.6.0", 31 | "async": "^3.1.0", 32 | "call-me-maybe": "^1.0.1", 33 | "grpc-create-metadata": "^4.0.0", 34 | "grpc-inspect": "^0.6.0", 35 | "lodash": "^4.17.14", 36 | "promisify-call": "^2.0.0" 37 | }, 38 | "peerDependencies": { 39 | "@grpc/grpc-js": "^1.2.5" 40 | }, 41 | "devDependencies": { 42 | "ava": "^3.15.0", 43 | "google-protobuf": "^3.8.0", 44 | "@grpc/grpc-js": "^1.2.5", 45 | "jsdoc-to-markdown": "^7.0.0", 46 | "standard": "^16.0.0" 47 | }, 48 | "directories": { 49 | "test": "test" 50 | }, 51 | "ava": { 52 | "files": [ 53 | "test/*.test.js" 54 | ] 55 | }, 56 | "engines": { 57 | "node": ">=14.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /readme.hbs: -------------------------------------------------------------------------------- 1 | # grpc-caller 2 | 3 | An improved [gRPC](http://www.grpc.io) client. 4 | 5 | [![npm version](https://img.shields.io/npm/v/grpc-caller.svg?style=flat-square)](https://www.npmjs.com/package/grpc-caller) 6 | [![build status](https://img.shields.io/travis/bojand/grpc-caller/master.svg?style=flat-square)](https://travis-ci.org/bojand/grpc-caller) 7 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](https://standardjs.com) 8 | [![License](https://img.shields.io/github/license/bojand/grpc-caller.svg?style=flat-square)](https://raw.githubusercontent.com/bojand/grpc-caller/master/LICENSE) 9 | 10 | #### Features 11 | 12 | * Promisifies request / response (Unary) calls if no callback is supplied 13 | * Promisifies request stream / response calls if no callback is supplied 14 | * Automatically converts plain javascript object to metadata in calls. 15 | * Adds optional retry functionality to request / response (Unary) calls. 16 | * Exposes expanded `Request` API for collecting metadata and status. 17 | 18 | ## Installation 19 | 20 | ``` 21 | $ npm install grpc-caller 22 | ``` 23 | 24 | ## Overview 25 | 26 | #### Improved unary calls 27 | 28 | Works as standard gRPC client: 29 | 30 | ```js 31 | const caller = require('grpc-caller') 32 | const PROTO_PATH = path.resolve(__dirname, './protos/helloworld.proto') 33 | const client = caller('0.0.0.0:50051', PROTO_PATH, 'Greeter') 34 | client.sayHello({ name: 'Bob' }, (err, res) => { 35 | console.log(res) 36 | }) 37 | ``` 38 | 39 | For unary calls, also promisified if callback is not provided: 40 | 41 | ```js 42 | client.sayHello({ name: 'Bob' }) 43 | .then(res => console.log(res)) 44 | ``` 45 | 46 | Which means means you can use is with `async / await` 47 | 48 | ```js 49 | const res = await client.sayHello({ name: 'Bob' }) 50 | console.log(res) 51 | ``` 52 | 53 | For Unary calls we expose `retry` option identical to [async.retry](http://caolan.github.io/async/docs.html#retry). 54 | 55 | ``` 56 | const res = await client.sayHello({ name: 'Bob' }, {}, { retry: 3 }) 57 | console.log(res) 58 | ``` 59 | 60 | #### Improved request stream / response calls 61 | 62 | Lets say we have a remote call `writeStuff` that accepts a stream of messages 63 | and returns some result based on processing of the stream input. 64 | 65 | Works as standard gRPC client: 66 | 67 | ```js 68 | const call = client.writeStuff((err, res) => { 69 | if (err) console.error(err) 70 | console.log(res) 71 | }) 72 | 73 | // ... write stuff to call 74 | ``` 75 | 76 | If no callback is provided we promisify the call such that it returns an **object 77 | with two properties** `call` and `res` such that: 78 | 79 | * `call` - the standard stream to write to as returned normally by grpc 80 | * `res` - a promise that's resolved / rejected when the call is finished, in place of the callback. 81 | 82 | Using destructuring we can do something like: 83 | 84 | ```js 85 | const { call, res } = client.writeStuff() 86 | res 87 | .then(res => console.log(res)) 88 | .catch(err => console.error(err)) 89 | 90 | // ... write stuff to call 91 | ``` 92 | 93 | This means we can abstract the whole operation into a nicer promise returning 94 | async function to use with `async / await` 95 | 96 | ```js 97 | async function writeStuff() { 98 | const { call, res } = client.writeStuff() 99 | // ... write stuff to call 100 | return res 101 | } 102 | 103 | const res = await writeStuff() 104 | console.log(res) 105 | ``` 106 | 107 | #### Automatic `Metadata` creation 108 | 109 | All standard gRPC client calls accept [`Metadata`](http://www.grpc.io/grpc/node/module-src_metadata-Metadata.html) 110 | as first or second parameter (depending on the call type). However one has to 111 | manually create the Metadata object. This module uses 112 | [grpc-create-metadata](https://www.github.com/bojand/grpc-create-metadata) 113 | to automatically create Metadata if plain Javascript object is passed in. 114 | 115 | ```js 116 | // the 2nd parameter will automatically be converted to gRPC Metadata and 117 | // included in the request 118 | const res = await client.sayHello({ name: 'Bob' }, { requestid: 'my-request-id-123' }) 119 | console.log(res) 120 | ``` 121 | 122 | We can still pass an actual `Metadata` object and it will be used as is: 123 | 124 | ```js 125 | const meta = new grpc.Metadata() 126 | meta.add('requestid', 'my-request-id-123') 127 | const res = await client.sayHello({ name: 'Bob' }, meta) 128 | console.log(res) 129 | ``` 130 | 131 | ## Request API 132 | 133 | In addition to simple API above, the library provides a more detailed `"Request"` API that can 134 | be used to control the call details. The API can only be used for Unary and 135 | request streaming calls. 136 | 137 | #### Unary calls 138 | 139 | ```js 140 | const req = new client 141 | .Request('sayHello', { name: 'Bob' }) // call method name and argument 142 | .withMetadata({ requestId: 'bar-123' }) // call request metadata 143 | .withResponseMetadata(true) // we want to collect response metadata 144 | .withResponseStatus(true) // we want to collect the response status 145 | .withRetry(5) // retry options 146 | 147 | const res = await req.exec() 148 | // res is an instance of our `Response` 149 | // we can also call exec() using a callback 150 | 151 | console.log(res.response) // the actual response data { message: 'Hello Bob!' } 152 | console.log(res.metadata) // the response metadata 153 | console.log(res.status) // the response status 154 | console.log(res.call) // the internal gRPC call 155 | ``` 156 | 157 | #### Request streaming calls 158 | 159 | In case of request streaming calls if `exec()` is called with a callback the gRPC `call` stream is returned. 160 | If no callback is provided an object is returned with `call` property being the call stream and `res` 161 | property being a Promise fulfilled when the call is completed. There is no `retry` option for 162 | request streaming calls. 163 | 164 | ```js 165 | 166 | const req = new client.Request('writeStuff') // the call method name 167 | .withMetadata({ requestId: 'bar-123' }) // the call request metadata 168 | .withResponseMetadata(true) // we want to collect response metadata 169 | .withResponseStatus(true) // we want to collect the response status 170 | 171 | const { call, res: resPromise } = req.exec() 172 | 173 | // ... write data to call 174 | 175 | const res = await resPromise // res is our `Response` 176 | 177 | console.log(res.response) // the actual response data 178 | console.log(res.metadata) // the response metadata 179 | console.log(res.status) // the response status 180 | console.log(res.call) // the internal gRPC call 181 | ``` 182 | 183 | ## API Reference 184 | 185 | {{>all-docs~}} 186 | 187 | 188 | ## License 189 | 190 | Apache-2.0 191 | -------------------------------------------------------------------------------- /test/basics.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const path = require('path') 3 | const async = require('async') 4 | const grpc = require('@grpc/grpc-js') 5 | const protoLoader = require('@grpc/proto-loader') 6 | 7 | const caller = require('../') 8 | 9 | const PROTO_PATH = path.resolve(__dirname, './protos/helloworld.proto') 10 | 11 | const packageDefinition = protoLoader.loadSync(PROTO_PATH) 12 | const helloproto = grpc.loadPackageDefinition(packageDefinition).helloworld 13 | 14 | const apps = [] 15 | 16 | function getRandomInt (min, max) { 17 | return Math.floor(Math.random() * (max - min + 1)) + min 18 | } 19 | 20 | function getHost (port) { 21 | return '0.0.0.0:'.concat(port || getRandomInt(1000, 60000)) 22 | } 23 | 24 | const STATIC_HOST = getHost() 25 | const DYNAMIC_HOST = getHost() 26 | 27 | test.before('should dynamically create service', t => { 28 | function sayHello (call, callback) { 29 | callback(null, { message: 'Hello ' + call.request.name }) 30 | } 31 | 32 | const server = new grpc.Server() 33 | server.addService(helloproto.Greeter.service, { sayHello: sayHello }) 34 | server.bindAsync(DYNAMIC_HOST, grpc.ServerCredentials.createInsecure(), err => { 35 | t.falsy(err) 36 | server.start() 37 | apps.push(server) 38 | }) 39 | }) 40 | 41 | test.before('should statically create service', t => { 42 | const messages = require('./static/helloworld_pb') 43 | const services = require('./static/helloworld_grpc_pb') 44 | 45 | function sayHello (call, callback) { 46 | const reply = new messages.HelloReply() 47 | reply.setMessage('Hello ' + call.request.getName()) 48 | callback(null, reply) 49 | } 50 | 51 | const server = new grpc.Server() 52 | server.addService(services.GreeterService, { sayHello: sayHello }) 53 | server.bindAsync(STATIC_HOST, grpc.ServerCredentials.createInsecure(), err => { 54 | t.falsy(err) 55 | server.start() 56 | apps.push(server) 57 | }) 58 | }) 59 | 60 | test.cb('call dynamic service using callback', t => { 61 | t.plan(4) 62 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'Greeter') 63 | client.sayHello({ name: 'Bob' }, (err, response) => { 64 | t.falsy(err) 65 | t.truthy(response) 66 | t.truthy(response.message) 67 | t.is(response.message, 'Hello Bob') 68 | t.end() 69 | }) 70 | }) 71 | 72 | test.cb('call dynamic service using callback created using package', t => { 73 | t.plan(4) 74 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'helloworld.Greeter') 75 | client.sayHello({ name: 'Bob' }, (err, response) => { 76 | t.falsy(err) 77 | t.truthy(response) 78 | t.truthy(response.message) 79 | t.is(response.message, 'Hello Bob') 80 | t.end() 81 | }) 82 | }) 83 | 84 | test.cb('call dynamic service using callback and load options', t => { 85 | t.plan(4) 86 | const client = caller(DYNAMIC_HOST, { load: {}, file: PROTO_PATH }, 'Greeter') 87 | client.sayHello({ name: 'Root' }, (err, response) => { 88 | t.falsy(err) 89 | t.truthy(response) 90 | t.truthy(response.message) 91 | t.is(response.message, 'Hello Root') 92 | t.end() 93 | }) 94 | }) 95 | 96 | test.cb('call static service using callback', t => { 97 | t.plan(5) 98 | 99 | const messages = require('./static/helloworld_pb') 100 | const services = require('./static/helloworld_grpc_pb') 101 | 102 | const client = caller(STATIC_HOST, services.GreeterClient) 103 | 104 | const request = new messages.HelloRequest() 105 | request.setName('Jane') 106 | client.sayHello(request, (err, response) => { 107 | t.falsy(err) 108 | t.truthy(response) 109 | t.truthy(response.getMessage) 110 | const msg = response.getMessage() 111 | t.truthy(msg) 112 | t.is(msg, 'Hello Jane') 113 | t.end() 114 | }) 115 | }) 116 | 117 | test('call dynamic service using async', async t => { 118 | t.plan(3) 119 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'Greeter') 120 | const response = await client.sayHello({ name: 'Bob' }) 121 | t.truthy(response) 122 | t.truthy(response.message) 123 | t.is(response.message, 'Hello Bob') 124 | }) 125 | 126 | test('call dynamic service using async and load options', async t => { 127 | t.plan(3) 128 | const client = caller(DYNAMIC_HOST, { load: {}, file: PROTO_PATH }, 'Greeter') 129 | const response = await client.sayHello({ name: 'Root' }) 130 | t.truthy(response) 131 | t.truthy(response.message) 132 | t.is(response.message, 'Hello Root') 133 | }) 134 | 135 | test('call static service using async', async t => { 136 | t.plan(4) 137 | 138 | const messages = require('./static/helloworld_pb') 139 | const services = require('./static/helloworld_grpc_pb') 140 | 141 | const client = caller(STATIC_HOST, services.GreeterClient) 142 | 143 | const request = new messages.HelloRequest() 144 | request.setName('Jane') 145 | const response = await client.sayHello(request) 146 | t.truthy(response) 147 | t.truthy(response.getMessage) 148 | const msg = response.getMessage() 149 | t.truthy(msg) 150 | t.is(msg, 'Hello Jane') 151 | }) 152 | 153 | test.after.always.cb('guaranteed cleanup', t => { 154 | async.each(apps, (app, ascb) => app.tryShutdown(ascb), t.end) 155 | }) 156 | -------------------------------------------------------------------------------- /test/defaults.test.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js') 2 | 3 | const caller = require('../') 4 | const test = require('ava') 5 | const path = require('path') 6 | const async = require('async') 7 | 8 | const PROTO_PATH = path.resolve(__dirname, './protos/helloworld.proto') 9 | 10 | const apps = [] 11 | 12 | function getRandomInt (min, max) { 13 | return Math.floor(Math.random() * (max - min + 1)) + min 14 | } 15 | 16 | function getHost (port) { 17 | return '0.0.0.0:'.concat(port || getRandomInt(1000, 60000)) 18 | } 19 | 20 | const TEST_HOST = getHost() 21 | 22 | test.before('start test servic', t => { 23 | const messages = require('./static/helloworld_pb') 24 | const services = require('./static/helloworld_grpc_pb') 25 | 26 | function sayHello (call, callback) { 27 | const reply = new messages.HelloReply() 28 | let responceMessage = '' 29 | 30 | if (call.metadata.get('foo').length > 0) { responceMessage = `${call.metadata.get('foo')} -> ${responceMessage}` } 31 | 32 | responceMessage = `${responceMessage}Hello ${call.request.getName()}` 33 | 34 | if (call.metadata.get('ping').length > 0) { responceMessage = `${responceMessage} -> ${call.metadata.get('ping')}` } 35 | 36 | reply.setMessage(responceMessage) 37 | callback(null, reply) 38 | } 39 | 40 | const server = new grpc.Server() 41 | server.addService(services.GreeterService, { sayHello: sayHello }) 42 | server.bindAsync(TEST_HOST, grpc.ServerCredentials.createInsecure(), err => { 43 | t.falsy(err) 44 | server.start() 45 | apps.push(server) 46 | }) 47 | }) 48 | 49 | test.cb('should pass default metadata', t => { 50 | t.plan(4) 51 | const client = caller(TEST_HOST, PROTO_PATH, 'helloworld.Greeter', false, {}, { metadata: { foo: 'bar' } }) 52 | client.sayHello({ name: 'Bob' }, (err, response) => { 53 | t.falsy(err) 54 | t.truthy(response) 55 | t.truthy(response.message) 56 | t.is(response.message, 'bar -> Hello Bob') 57 | t.end() 58 | }) 59 | }) 60 | 61 | test.cb('should pass extend metadata (simple object)', t => { 62 | t.plan(4) 63 | const client = caller(TEST_HOST, PROTO_PATH, 'helloworld.Greeter', false, {}, { metadata: { foo: 'bar', ping: 'pong' } }) 64 | client.sayHello({ name: 'Bob' }, { foo: 'bar2000' }, (err, response) => { 65 | t.falsy(err) 66 | t.truthy(response) 67 | t.truthy(response.message) 68 | t.is(response.message, 'bar2000 -> Hello Bob -> pong') 69 | t.end() 70 | }) 71 | }) 72 | 73 | test.cb('should pass extend metadata (grpc.Metadata)', t => { 74 | t.plan(4) 75 | const meta = new grpc.Metadata() 76 | meta.add('ping', 'master') 77 | 78 | const client = caller(TEST_HOST, PROTO_PATH, 'helloworld.Greeter', false, {}, { metadata: { foo: 'bar' } }) 79 | client.sayHello({ name: 'Bob' }, meta, (err, response) => { 80 | t.falsy(err) 81 | t.truthy(response) 82 | t.truthy(response.message) 83 | t.is(response.message, 'bar -> Hello Bob -> master') 84 | t.end() 85 | }) 86 | }) 87 | 88 | test.cb('load interceptors and default metadata', t => { 89 | t.plan(5) 90 | 91 | const interceptor = (options, nextCall) => 92 | new grpc.InterceptingCall(nextCall(options), { 93 | sendMessage: (message, next) => { 94 | t.is(message.name, 'Bob') 95 | next({ name: message.name + 2 }) 96 | } 97 | }) 98 | 99 | const client = caller(TEST_HOST, PROTO_PATH, 'helloworld.Greeter', false, {}, { 100 | metadata: { foo: 'bar' }, 101 | options: { interceptors: [interceptor] } 102 | }) 103 | 104 | client.sayHello({ name: 'Bob' }, (err, response) => { 105 | t.falsy(err) 106 | t.truthy(response) 107 | t.truthy(response.message) 108 | t.is(response.message, 'bar -> Hello Bob2') 109 | t.end() 110 | }) 111 | }) 112 | 113 | test.cb('load interceptors, default metadata and call metadata', t => { 114 | t.plan(5) 115 | 116 | const interceptor = (options, nextCall) => 117 | new grpc.InterceptingCall(nextCall(options), { 118 | sendMessage: (message, next) => { 119 | t.is(message.name, 'Bob') 120 | next({ name: message.name + 2 }) 121 | } 122 | }) 123 | 124 | const client = caller(TEST_HOST, PROTO_PATH, 'helloworld.Greeter', false, {}, { 125 | metadata: { foo: 'bar' }, 126 | options: { interceptors: [interceptor] } 127 | }) 128 | 129 | client.sayHello({ name: 'Bob' }, { ping: 'meta' }, (err, response) => { 130 | t.falsy(err) 131 | t.truthy(response) 132 | t.truthy(response.message) 133 | t.is(response.message, 'bar -> Hello Bob2 -> meta') 134 | t.end() 135 | }) 136 | }) 137 | 138 | test('async options interceptors, default metadata and call metadata', async t => { 139 | t.plan(4) 140 | 141 | const interceptor = (options, nextCall) => 142 | new grpc.InterceptingCall(nextCall(options), { 143 | sendMessage: (message, next) => { 144 | t.is(message.name, 'Bob') 145 | next({ name: message.name + 2 }) 146 | } 147 | }) 148 | 149 | const credentials = grpc.credentials.createInsecure() 150 | const options = { 151 | interceptors: [interceptor] 152 | } 153 | 154 | const client = caller(TEST_HOST, PROTO_PATH, 'helloworld.Greeter', credentials, options, { 155 | metadata: { foo: 'bar' } 156 | }) 157 | 158 | const response = await client.sayHello({ name: 'Bob' }, { ping: 'meta' }) 159 | t.truthy(response) 160 | t.truthy(response.message) 161 | t.is(response.message, 'bar -> Hello Bob2 -> meta') 162 | }) 163 | 164 | test('static async options interceptors, default metadata and call metadata', async t => { 165 | t.plan(4) 166 | 167 | const messages = require('./static/helloworld_pb') 168 | const services = require('./static/helloworld_grpc_pb') 169 | 170 | const interceptor = (options, nextCall) => 171 | new grpc.InterceptingCall(nextCall(options), { 172 | sendMessage: (message, next) => { 173 | const name = message.getName() 174 | t.is(name, 'Bob') 175 | message.setName(name + 2) 176 | next(message) 177 | } 178 | }) 179 | 180 | const credentials = grpc.credentials.createInsecure() 181 | const options = { 182 | interceptors: [interceptor] 183 | } 184 | 185 | const serviceClient = new services.GreeterClient(TEST_HOST, credentials, options) 186 | 187 | const client = caller.wrap(serviceClient, { foo: 'bar' }, options) 188 | 189 | const request = new messages.HelloRequest() 190 | request.setName('Bob') 191 | 192 | const response = await client.sayHello(request, { ping: 'meta' }) 193 | t.truthy(response) 194 | t.truthy(response.getMessage()) 195 | 196 | const msg = response.getMessage() 197 | t.is(msg, 'bar -> Hello Bob2 -> meta') 198 | }) 199 | 200 | test.after.always.cb('guaranteed cleanup', t => { 201 | async.each(apps, (app, ascb) => app.tryShutdown(ascb), t.end) 202 | }) 203 | -------------------------------------------------------------------------------- /test/duplex.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const test = require('ava') 3 | const path = require('path') 4 | const async = require('async') 5 | const grpc = require('@grpc/grpc-js') 6 | 7 | const protoLoader = require('@grpc/proto-loader') 8 | 9 | const caller = require('../') 10 | 11 | const PROTO_PATH = path.resolve(__dirname, './protos/duplex.proto') 12 | const packageDefinition = protoLoader.loadSync(PROTO_PATH) 13 | const argProto = grpc.loadPackageDefinition(packageDefinition).argservice 14 | 15 | const apps = [] 16 | 17 | const data = [ 18 | { message: '1 foo' }, 19 | { message: '2 bar' }, 20 | { message: '3 asd' }, 21 | { message: '4 qwe' }, 22 | { message: '5 rty' }, 23 | { message: '6 zxc' } 24 | ] 25 | 26 | function getRandomInt (min, max) { 27 | return Math.floor(Math.random() * (max - min + 1)) + min 28 | } 29 | 30 | function getHost (port) { 31 | return '0.0.0.0:'.concat(port || getRandomInt(1000, 60000)) 32 | } 33 | 34 | const DYNAMIC_HOST = getHost() 35 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'ArgService') 36 | 37 | test.before('should dynamically create service', t => { 38 | function processStuff (call) { 39 | let meta = null 40 | if (call.metadata) { 41 | const reqMeta = call.metadata.getMap() 42 | if (reqMeta['user-agent']) { 43 | delete reqMeta['user-agent'] 44 | } 45 | if (!_.isEmpty(reqMeta)) { 46 | meta = JSON.stringify(reqMeta) 47 | } 48 | } 49 | 50 | call.on('data', d => { 51 | call.pause() 52 | _.delay(() => { 53 | const ret = { message: d.message.toUpperCase() } 54 | if (meta) { 55 | ret.metadata = meta 56 | } 57 | call.write(ret) 58 | call.resume() 59 | }, _.random(50, 150)) 60 | }) 61 | 62 | call.on('end', () => { 63 | _.delay(() => { 64 | // async.doWhilst(cb => process.nextTick(cb), () => { 65 | // return counter < 5 66 | // }, () => { 67 | call.end() 68 | // }) 69 | }, 200) 70 | }) 71 | } 72 | 73 | const server = new grpc.Server() 74 | server.addService(argProto.ArgService.service, { processStuff }) 75 | server.bindAsync(DYNAMIC_HOST, grpc.ServerCredentials.createInsecure(), err => { 76 | t.falsy(err) 77 | server.start() 78 | apps.push(server) 79 | }) 80 | }) 81 | 82 | test.cb('Duplex: call service using just an argument', t => { 83 | t.plan(1) 84 | let resData = [] 85 | 86 | const call = client.processStuff() 87 | 88 | call.on('data', d => { 89 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 90 | resData.push({ message: d.message, metadata }) 91 | }) 92 | 93 | call.on('end', () => { 94 | resData = _.sortBy(resData, 'message') 95 | 96 | let expected = _.cloneDeep(data) 97 | expected = _.map(expected, d => { 98 | d.message = d.message.toUpperCase() 99 | d.metadata = '' 100 | return d 101 | }) 102 | 103 | t.deepEqual(resData, expected) 104 | t.end() 105 | }) 106 | 107 | async.eachSeries(data, (d, asfn) => { 108 | _.delay(() => { 109 | call.write(d) 110 | asfn() 111 | }, _.random(10, 50)) 112 | }, () => { 113 | _.delay(() => call.end(), 50) 114 | }) 115 | }) 116 | 117 | test.cb('call service with metadata as plain object', t => { 118 | t.plan(1) 119 | let resData = [] 120 | const ts = new Date().getTime() 121 | const call = client.processStuff({ requestId: 'bar-123', timestamp: ts }) 122 | 123 | call.on('data', d => { 124 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 125 | resData.push({ message: d.message, metadata }) 126 | }) 127 | 128 | call.on('end', () => { 129 | resData = _.sortBy(resData, 'message') 130 | 131 | let expected = _.cloneDeep(data) 132 | expected = _.map(expected, d => { 133 | d.message = d.message.toUpperCase() 134 | d.metadata = { requestid: 'bar-123', timestamp: ts.toString() } 135 | return d 136 | }) 137 | 138 | t.deepEqual(resData, expected) 139 | t.end() 140 | }) 141 | 142 | async.eachSeries(data, (d, asfn) => { 143 | _.delay(cb => { 144 | call.write(d) 145 | asfn() 146 | }, _.random(50, 100)) 147 | }, () => { 148 | call.end() 149 | }) 150 | }) 151 | 152 | test.cb('call service with metadata as Metadata', t => { 153 | t.plan(1) 154 | let resData = [] 155 | const ts = new Date().getTime().toString() 156 | const reqMeta = new grpc.Metadata() 157 | reqMeta.add('requestId', 'bar-123') 158 | reqMeta.add('timestamp', ts) 159 | const call = client.processStuff(reqMeta) 160 | 161 | call.on('data', d => { 162 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 163 | resData.push({ message: d.message, metadata }) 164 | }) 165 | 166 | call.on('end', () => { 167 | resData = _.sortBy(resData, 'message') 168 | 169 | let expected = _.cloneDeep(data) 170 | expected = _.map(expected, d => { 171 | d.message = d.message.toUpperCase() 172 | d.metadata = { requestid: 'bar-123', timestamp: ts } 173 | return d 174 | }) 175 | 176 | t.deepEqual(resData, expected) 177 | t.end() 178 | }) 179 | 180 | async.eachSeries(data, (d, asfn) => { 181 | _.delay(() => { 182 | call.write(d) 183 | asfn() 184 | }, _.random(10, 50)) 185 | }, () => { 186 | _.delay(() => call.end(), 50) 187 | }) 188 | }) 189 | 190 | test.cb('call service with metadata as plain object and options object', t => { 191 | t.plan(1) 192 | let resData = [] 193 | const ts = new Date().getTime() 194 | const call = client.processStuff({ requestId: 'bar-123', timestamp: ts }, { some: 'blah' }) 195 | 196 | call.on('data', d => { 197 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 198 | resData.push({ message: d.message, metadata }) 199 | }) 200 | 201 | call.on('end', () => { 202 | resData = _.sortBy(resData, 'message') 203 | 204 | let expected = _.cloneDeep(data) 205 | expected = _.map(expected, d => { 206 | d.message = d.message.toUpperCase() 207 | d.metadata = { requestid: 'bar-123', timestamp: ts.toString() } 208 | return d 209 | }) 210 | 211 | t.deepEqual(resData, expected) 212 | t.end() 213 | }) 214 | 215 | async.eachSeries(data, (d, asfn) => { 216 | _.delay(() => { 217 | call.write(d) 218 | asfn() 219 | }, _.random(10, 50)) 220 | }, () => { 221 | _.delay(() => call.end(), 50) 222 | }) 223 | }) 224 | 225 | test.cb('call service with metadata as Metadata and options object', t => { 226 | t.plan(1) 227 | let resData = [] 228 | const ts = new Date().getTime().toString() 229 | const reqMeta = new grpc.Metadata() 230 | reqMeta.add('requestId', 'bar-123') 231 | reqMeta.add('timestamp', ts) 232 | 233 | const call = client.processStuff(reqMeta, { some: 'blah' }) 234 | 235 | call.on('data', d => { 236 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 237 | resData.push({ message: d.message, metadata }) 238 | }) 239 | 240 | call.on('end', () => { 241 | resData = _.sortBy(resData, 'message') 242 | 243 | let expected = _.cloneDeep(data) 244 | expected = _.map(expected, d => { 245 | d.message = d.message.toUpperCase() 246 | d.metadata = { requestid: 'bar-123', timestamp: ts } 247 | return d 248 | }) 249 | 250 | t.deepEqual(resData, expected) 251 | t.end() 252 | }) 253 | 254 | async.eachSeries(data, (d, asfn) => { 255 | _.delay(() => { 256 | call.write(d) 257 | asfn() 258 | }, _.random(10, 50)) 259 | }, () => { 260 | _.delay(() => call.end(), 50) 261 | }) 262 | }) 263 | 264 | test.after.always.cb('guaranteed cleanup', t => { 265 | async.each(apps, (app, ascb) => app.tryShutdown(ascb), t.end) 266 | }) 267 | -------------------------------------------------------------------------------- /test/protos/duplex.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package argservice; 4 | 5 | service ArgService { 6 | rpc ProcessStuff(stream ArgRequest) returns (stream ArgReply) {} 7 | } 8 | 9 | message ArgRequest { 10 | string message = 1; 11 | } 12 | 13 | message ArgReply { 14 | string message = 1; 15 | string metadata = 2; 16 | } 17 | -------------------------------------------------------------------------------- /test/protos/helloworld.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015, Google Inc. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // * Neither the name of Google Inc. nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | syntax = "proto3"; 31 | 32 | package helloworld; 33 | 34 | // The greeting service definition. 35 | service Greeter { 36 | // Sends a greeting 37 | rpc SayHello (HelloRequest) returns (HelloReply) {} 38 | } 39 | 40 | // The request message containing the user's name. 41 | message HelloRequest { 42 | string name = 1; 43 | } 44 | 45 | // The response message containing the greetings 46 | message HelloReply { 47 | string message = 1; 48 | } 49 | -------------------------------------------------------------------------------- /test/protos/reqres.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package argservice; 4 | 5 | service ArgService { 6 | rpc DoSomething (ArgRequest) returns (ArgReply) {} 7 | } 8 | 9 | message ArgRequest { 10 | string message = 1; 11 | } 12 | 13 | message ArgReply { 14 | string message = 1; 15 | string metadata = 2; 16 | } 17 | -------------------------------------------------------------------------------- /test/protos/reqstream.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package argservice; 4 | 5 | service ArgService { 6 | rpc WriteStuff(stream ArgRequest) returns (ArgReply) {} 7 | } 8 | 9 | message ArgRequest { 10 | string message = 1; 11 | } 12 | 13 | message ArgReply { 14 | string message = 1; 15 | string metadata = 2; 16 | } 17 | -------------------------------------------------------------------------------- /test/protos/resstream.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package argservice; 4 | 5 | service ArgService { 6 | rpc ListStuff(ArgRequest) returns (stream ArgReply) {} 7 | } 8 | 9 | message ArgRequest { 10 | string message = 1; 11 | } 12 | 13 | message ArgReply { 14 | string message = 1; 15 | string metadata = 2; 16 | } 17 | -------------------------------------------------------------------------------- /test/reqres.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const test = require('ava') 3 | const path = require('path') 4 | const async = require('async') 5 | const grpc = require('@grpc/grpc-js') 6 | 7 | const protoLoader = require('@grpc/proto-loader') 8 | 9 | const caller = require('../') 10 | 11 | const PROTO_PATH = path.resolve(__dirname, './protos/reqres.proto') 12 | const packageDefinition = protoLoader.loadSync(PROTO_PATH) 13 | const argProto = grpc.loadPackageDefinition(packageDefinition).argservice 14 | 15 | const apps = [] 16 | 17 | function getRandomInt (min, max) { 18 | return Math.floor(Math.random() * (max - min + 1)) + min 19 | } 20 | 21 | function getHost (port) { 22 | return '0.0.0.0:'.concat(port || getRandomInt(1000, 60000)) 23 | } 24 | 25 | const DYNAMIC_HOST = getHost() 26 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'ArgService') 27 | 28 | test.before('should dynamically create service', t => { 29 | function doSomething (call, callback) { 30 | const ret = { message: call.request.message } 31 | 32 | const md = new grpc.Metadata() 33 | md.set('headerMD', 'headerValue') 34 | call.sendMetadata(md) 35 | 36 | if (call.metadata) { 37 | const meta = call.metadata.getMap() 38 | if (meta['user-agent']) { 39 | delete meta['user-agent'] 40 | } 41 | if (!_.isEmpty(meta)) { 42 | ret.metadata = JSON.stringify(meta) 43 | } 44 | } 45 | 46 | const md2 = new grpc.Metadata() 47 | md2.set('trailerMD', 'trailerValue') 48 | 49 | callback(null, ret, md2) 50 | } 51 | 52 | const server = new grpc.Server() 53 | server.addService(argProto.ArgService.service, { doSomething }) 54 | server.bindAsync(DYNAMIC_HOST, grpc.ServerCredentials.createInsecure(), err => { 55 | t.falsy(err) 56 | server.start() 57 | apps.push(server) 58 | }) 59 | }) 60 | 61 | test.cb('call service using callback and just an argument', t => { 62 | t.plan(5) 63 | client.doSomething({ message: 'Hello' }, (err, response) => { 64 | t.falsy(err) 65 | t.truthy(response) 66 | t.truthy(response.message) 67 | t.falsy(response.metadata) 68 | t.is(response.message, 'Hello') 69 | t.end() 70 | }) 71 | }) 72 | 73 | test('call service using async with just an argument', async t => { 74 | t.plan(4) 75 | const response = await client.doSomething({ message: 'Hi' }) 76 | t.truthy(response) 77 | t.truthy(response.message) 78 | t.falsy(response.metadata) 79 | t.is(response.message, 'Hi') 80 | }) 81 | 82 | test.cb('call service using callback with metadata as plain object', t => { 83 | t.plan(6) 84 | const ts = new Date().getTime() 85 | client.doSomething({ message: 'Hello' }, { requestId: 'bar-123', timestamp: ts }, (err, response) => { 86 | t.falsy(err) 87 | t.truthy(response) 88 | t.truthy(response.message) 89 | t.is(response.message, 'Hello') 90 | t.truthy(response.metadata) 91 | const metadata = JSON.parse(response.metadata) 92 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 93 | t.deepEqual(metadata, expected) 94 | t.end() 95 | }) 96 | }) 97 | 98 | test('call service using async with metadata as plain object', async t => { 99 | t.plan(5) 100 | const ts = new Date().getTime() 101 | const response = await client.doSomething({ message: 'Hi' }, { requestId: 'bar-123', timestamp: ts }) 102 | t.truthy(response) 103 | t.truthy(response.message) 104 | t.is(response.message, 'Hi') 105 | t.truthy(response.metadata) 106 | const metadata = JSON.parse(response.metadata) 107 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 108 | t.deepEqual(metadata, expected) 109 | }) 110 | 111 | test.cb('call service using callback with metadata as Metadata', t => { 112 | t.plan(6) 113 | const ts = new Date().getTime().toString() 114 | const reqMeta = new grpc.Metadata() 115 | reqMeta.add('requestId', 'bar-123') 116 | reqMeta.add('timestamp', ts) 117 | client.doSomething({ message: 'Hello' }, reqMeta, (err, response) => { 118 | t.falsy(err) 119 | t.truthy(response) 120 | t.truthy(response.message) 121 | t.is(response.message, 'Hello') 122 | t.truthy(response.metadata) 123 | const metadata = JSON.parse(response.metadata) 124 | const expected = { requestid: 'bar-123', timestamp: ts } 125 | t.deepEqual(metadata, expected) 126 | t.end() 127 | }) 128 | }) 129 | 130 | test('call service using async with metadata as Metadata', async t => { 131 | t.plan(5) 132 | const ts = new Date().getTime().toString() 133 | const reqMeta = new grpc.Metadata() 134 | reqMeta.add('requestId', 'bar-123') 135 | reqMeta.add('timestamp', ts) 136 | const response = await client.doSomething({ message: 'Hi' }, reqMeta) 137 | t.truthy(response) 138 | t.truthy(response.message) 139 | t.is(response.message, 'Hi') 140 | t.truthy(response.metadata) 141 | const metadata = JSON.parse(response.metadata) 142 | const expected = { requestid: 'bar-123', timestamp: ts } 143 | t.deepEqual(metadata, expected) 144 | }) 145 | 146 | test.cb('call service using callback with metadata as plain object and options object', t => { 147 | t.plan(6) 148 | const ts = new Date().getTime() 149 | client.doSomething({ message: 'Hello' }, { requestId: 'bar-123', timestamp: ts }, { some: 'blah' }, (err, response) => { 150 | t.falsy(err) 151 | t.truthy(response) 152 | t.truthy(response.message) 153 | t.is(response.message, 'Hello') 154 | t.truthy(response.metadata) 155 | const metadata = JSON.parse(response.metadata) 156 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 157 | t.deepEqual(metadata, expected) 158 | t.end() 159 | }) 160 | }) 161 | 162 | test('call service using async with metadata as plain object and options object', async t => { 163 | t.plan(5) 164 | const ts = new Date().getTime() 165 | const response = await client.doSomething({ message: 'Hi' }, { requestId: 'bar-123', timestamp: ts }, { some: 'blah' }) 166 | t.truthy(response) 167 | t.truthy(response.message) 168 | t.is(response.message, 'Hi') 169 | t.truthy(response.metadata) 170 | const metadata = JSON.parse(response.metadata) 171 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 172 | t.deepEqual(metadata, expected) 173 | }) 174 | 175 | test.cb('call service using callback with metadata as Metadata and options object', t => { 176 | t.plan(6) 177 | const ts = new Date().getTime().toString() 178 | const reqMeta = new grpc.Metadata() 179 | reqMeta.add('requestId', 'bar-123') 180 | reqMeta.add('timestamp', ts) 181 | client.doSomething({ message: 'Hello' }, reqMeta, { some: 'blah' }, (err, response) => { 182 | t.falsy(err) 183 | t.truthy(response) 184 | t.truthy(response.message) 185 | t.is(response.message, 'Hello') 186 | t.truthy(response.metadata) 187 | const metadata = JSON.parse(response.metadata) 188 | const expected = { requestid: 'bar-123', timestamp: ts } 189 | t.deepEqual(metadata, expected) 190 | t.end() 191 | }) 192 | }) 193 | 194 | test('call service using async with metadata as Metadata and options object', async t => { 195 | t.plan(5) 196 | const ts = new Date().getTime().toString() 197 | const reqMeta = new grpc.Metadata() 198 | reqMeta.add('requestId', 'bar-123') 199 | reqMeta.add('timestamp', ts) 200 | const response = await client.doSomething({ message: 'Hi' }, reqMeta, { some: 'blah' }) 201 | t.truthy(response) 202 | t.truthy(response.message) 203 | t.is(response.message, 'Hi') 204 | t.truthy(response.metadata) 205 | const metadata = JSON.parse(response.metadata) 206 | const expected = { requestid: 'bar-123', timestamp: ts } 207 | t.deepEqual(metadata, expected) 208 | }) 209 | 210 | test.cb('Request API: call service using callback and just an argument', t => { 211 | t.plan(9) 212 | const req = new client.Request('doSomething', { message: 'Hello' }) 213 | req.exec((err, res) => { 214 | t.falsy(err) 215 | const { response } = res 216 | t.truthy(res.response) 217 | t.truthy(res.call) 218 | t.falsy(res.metadata) 219 | t.falsy(res.status) 220 | t.truthy(response) 221 | t.truthy(response.message) 222 | t.falsy(response.metadata) 223 | t.is(response.message, 'Hello') 224 | t.end() 225 | }) 226 | }) 227 | 228 | test('Request API: call service using async with just an argument', async t => { 229 | t.plan(8) 230 | const req = new client.Request('doSomething', { message: 'Hi' }) 231 | const res = await req.exec() 232 | const { response } = res 233 | t.truthy(res.response) 234 | t.truthy(res.call) 235 | t.falsy(res.metadata) 236 | t.falsy(res.status) 237 | t.truthy(response) 238 | t.truthy(response.message) 239 | t.falsy(response.metadata) 240 | t.is(response.message, 'Hi') 241 | }) 242 | 243 | test.cb('Request API: call service using callback with metadata as plain object', t => { 244 | t.plan(9) 245 | const ts = new Date().getTime() 246 | const req = new client.Request('doSomething', { message: 'Hello' }) 247 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 248 | 249 | req.exec((err, res) => { 250 | t.falsy(err) 251 | const { response } = res 252 | t.truthy(res.call) 253 | t.falsy(res.metadata) 254 | t.falsy(res.status) 255 | t.truthy(response) 256 | t.truthy(response.message) 257 | t.is(response.message, 'Hello') 258 | t.truthy(response.metadata) 259 | const metadata = JSON.parse(response.metadata) 260 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 261 | t.deepEqual(metadata, expected) 262 | t.end() 263 | }) 264 | }) 265 | 266 | test('Request API: call service using async with metadata as plain object', async t => { 267 | t.plan(8) 268 | const ts = new Date().getTime() 269 | 270 | const req = new client.Request('doSomething', { message: 'Hi' }) 271 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 272 | 273 | const res = await req.exec() 274 | t.truthy(res.call) 275 | t.falsy(res.metadata) 276 | t.falsy(res.status) 277 | const { response } = res 278 | t.truthy(response) 279 | t.truthy(response.message) 280 | t.is(response.message, 'Hi') 281 | t.truthy(response.metadata) 282 | const metadata = JSON.parse(response.metadata) 283 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 284 | t.deepEqual(metadata, expected) 285 | }) 286 | 287 | test('Request API: with metadata option', async t => { 288 | t.plan(9) 289 | const ts = new Date().getTime() 290 | 291 | const req = new client.Request('doSomething', { message: 'Hi' }) 292 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 293 | .withResponseMetadata(true) 294 | 295 | const res = await req.exec() 296 | t.truthy(res.call) 297 | t.truthy(res.metadata) 298 | const md1 = res.metadata.getMap() 299 | const expectedMd = { headermd: 'headerValue' } 300 | t.is(md1.headermd, expectedMd.headermd) 301 | 302 | t.falsy(res.status) 303 | const { response } = res 304 | t.truthy(response) 305 | t.truthy(response.message) 306 | t.is(response.message, 'Hi') 307 | t.truthy(response.metadata) 308 | const metadata = JSON.parse(response.metadata) 309 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 310 | t.deepEqual(metadata, expected) 311 | }) 312 | 313 | test('Request API: with status option', async t => { 314 | t.plan(12) 315 | const ts = new Date().getTime() 316 | 317 | const req = new client.Request('doSomething', { message: 'Hi' }) 318 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 319 | .withResponseStatus(true) 320 | 321 | const res = await req.exec() 322 | t.truthy(res.call) 323 | t.falsy(res.metadata) 324 | t.truthy(res.status) 325 | t.is(res.status.code, 0) 326 | t.is(res.status.details, 'OK') 327 | t.truthy(res.status.metadata) 328 | const md1 = res.status.metadata.getMap() 329 | const expectedMd = { trailermd: 'trailerValue' } 330 | t.is(md1.headermd, expectedMd.headermd) 331 | 332 | const { response } = res 333 | t.truthy(response) 334 | t.truthy(response.message) 335 | t.is(response.message, 'Hi') 336 | t.truthy(response.metadata) 337 | const metadata = JSON.parse(response.metadata) 338 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 339 | t.deepEqual(metadata, expected) 340 | }) 341 | 342 | test('Request API: with metadata and status option', async t => { 343 | t.plan(12) 344 | const ts = new Date().getTime() 345 | 346 | const req = new client.Request('doSomething', { message: 'Hi' }) 347 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 348 | .withResponseMetadata(true) 349 | .withResponseStatus(true) 350 | 351 | const res = await req.exec() 352 | 353 | t.truthy(res.metadata) 354 | const md1 = res.metadata.getMap() 355 | const expectedMd = { headermd: 'headerValue' } 356 | t.is(md1.headermd, expectedMd.headermd) 357 | 358 | t.truthy(res.status) 359 | t.is(res.status.code, 0) 360 | t.is(res.status.details, 'OK') 361 | t.truthy(res.status.metadata) 362 | const md2 = res.status.metadata.getMap() 363 | const expectedMd2 = { trailermd: 'trailerValue' } 364 | t.deepEqual(md2, expectedMd2) 365 | 366 | const { response } = res 367 | t.truthy(response) 368 | t.truthy(response.message) 369 | t.is(response.message, 'Hi') 370 | t.truthy(response.metadata) 371 | const metadata = JSON.parse(response.metadata) 372 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 373 | t.deepEqual(metadata, expected) 374 | }) 375 | 376 | test.cb('Request API: with metadata and status option with callback', t => { 377 | t.plan(13) 378 | const ts = new Date().getTime() 379 | 380 | const req = new client.Request('doSomething', { message: 'Hi' }) 381 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 382 | .withResponseMetadata(true) 383 | .withResponseStatus(true) 384 | 385 | req.exec((err, res) => { 386 | t.falsy(err) 387 | t.truthy(res.metadata) 388 | const md1 = res.metadata.getMap() 389 | const expectedMd = { headermd: 'headerValue' } 390 | t.is(md1.headermd, expectedMd.headermd) 391 | 392 | t.truthy(res.status) 393 | t.is(res.status.code, 0) 394 | t.is(res.status.details, 'OK') 395 | t.truthy(res.status.metadata) 396 | const md2 = res.status.metadata.getMap() 397 | const expectedMd2 = { trailermd: 'trailerValue' } 398 | t.deepEqual(md2, expectedMd2) 399 | 400 | const { response } = res 401 | t.truthy(response) 402 | t.truthy(response.message) 403 | t.is(response.message, 'Hi') 404 | t.truthy(response.metadata) 405 | const metadata = JSON.parse(response.metadata) 406 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 407 | t.deepEqual(metadata, expected) 408 | t.end() 409 | }) 410 | }) 411 | 412 | test('Request API: expect to throw on unknown client method', t => { 413 | const error = t.throws(() => { 414 | const req = new client.Request('asdf', { message: 'Hi' }) 415 | .withMetadata({ requestId: 'bar-123' }) 416 | .withResponseMetadata(true) 417 | .withResponseStatus(true) 418 | 419 | req.exec() 420 | }) 421 | 422 | t.truthy(error) 423 | t.is(error.message, 'Invalid method: asdf') 424 | }) 425 | 426 | test.after.always.cb('guaranteed cleanup', t => { 427 | async.each(apps, (app, ascb) => app.tryShutdown(ascb), t.end) 428 | }) 429 | -------------------------------------------------------------------------------- /test/reqstream.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const test = require('ava') 3 | const path = require('path') 4 | const async = require('async') 5 | const grpc = require('@grpc/grpc-js') 6 | 7 | const protoLoader = require('@grpc/proto-loader') 8 | 9 | const caller = require('../') 10 | 11 | const PROTO_PATH = path.resolve(__dirname, './protos/reqstream.proto') 12 | const packageDefinition = protoLoader.loadSync(PROTO_PATH) 13 | const argProto = grpc.loadPackageDefinition(packageDefinition).argservice 14 | 15 | const apps = [] 16 | 17 | const data = [ 18 | { message: '1 foo' }, 19 | { message: '2 bar' }, 20 | { message: '3 asd' }, 21 | { message: '4 qwe' }, 22 | { message: '5 rty' }, 23 | { message: '6 zxc' } 24 | ] 25 | 26 | function getRandomInt (min, max) { 27 | return Math.floor(Math.random() * (max - min + 1)) + min 28 | } 29 | 30 | function getHost (port) { 31 | return '0.0.0.0:'.concat(port || getRandomInt(1000, 60000)) 32 | } 33 | 34 | const DYNAMIC_HOST = getHost() 35 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'ArgService') 36 | 37 | test.before('should dynamically create service', t => { 38 | function writeStuff (call, fn) { 39 | const md = new grpc.Metadata() 40 | md.set('headerMD', 'headerValue') 41 | call.sendMetadata(md) 42 | 43 | let meta = null 44 | if (call.metadata) { 45 | const reqMeta = call.metadata.getMap() 46 | if (reqMeta['user-agent']) { 47 | delete reqMeta['user-agent'] 48 | } 49 | if (!_.isEmpty(reqMeta)) { 50 | meta = JSON.stringify(reqMeta) 51 | } 52 | } 53 | 54 | let counter = 0 55 | const received = [] 56 | call.on('data', d => { 57 | counter += 1 58 | received.push(d.message) 59 | }) 60 | 61 | call.on('end', () => { 62 | const ret = { 63 | message: received.join(':').concat(':' + counter) 64 | } 65 | 66 | if (meta) { 67 | ret.metadata = meta 68 | } 69 | 70 | const md2 = new grpc.Metadata() 71 | md2.set('trailerMD', 'trailerValue') 72 | 73 | fn(null, ret, md2) 74 | }) 75 | } 76 | 77 | const server = new grpc.Server() 78 | server.addService(argProto.ArgService.service, { writeStuff }) 79 | server.bindAsync(DYNAMIC_HOST, grpc.ServerCredentials.createInsecure(), err => { 80 | t.falsy(err) 81 | server.start() 82 | apps.push(server) 83 | }) 84 | }) 85 | 86 | test.cb('Reqres: call service using just an argument', t => { 87 | t.plan(5) 88 | const call = client.writeStuff((err, res) => { 89 | t.falsy(err) 90 | t.truthy(res) 91 | t.truthy(res.message) 92 | t.falsy(res.metadata) 93 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 94 | t.end() 95 | }) 96 | 97 | async.eachSeries(data, (d, asfn) => { 98 | call.write(d) 99 | _.delay(asfn, _.random(50, 150)) 100 | }, () => { 101 | call.end() 102 | }) 103 | }) 104 | 105 | test.cb('promised call service using just an argument', t => { 106 | t.plan(4) 107 | const { call, res } = client.writeStuff() 108 | res.then(res => { 109 | t.truthy(res) 110 | t.truthy(res.message) 111 | t.falsy(res.metadata) 112 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 113 | t.end() 114 | }) 115 | 116 | async.eachSeries(data, (d, asfn) => { 117 | call.write(d) 118 | _.delay(asfn, _.random(50, 150)) 119 | }, () => { 120 | call.end() 121 | }) 122 | }) 123 | 124 | test('async call service using just an argument', async t => { 125 | t.plan(4) 126 | 127 | async function writeStuff () { 128 | const { call, res } = client.writeStuff() 129 | 130 | async.eachSeries(data, (d, asfn) => { 131 | call.write(d) 132 | _.delay(asfn, _.random(50, 150)) 133 | }, () => { 134 | call.end() 135 | }) 136 | 137 | return res 138 | } 139 | 140 | const result = await writeStuff() 141 | t.truthy(result) 142 | t.truthy(result.message) 143 | t.falsy(result.metadata) 144 | t.is(result.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 145 | }) 146 | 147 | test.cb('call service with metadata as plain object', t => { 148 | t.plan(6) 149 | const ts = new Date().getTime() 150 | const call = client.writeStuff({ requestId: 'bar-123', timestamp: ts }, (err, res) => { 151 | t.falsy(err) 152 | t.truthy(res) 153 | t.truthy(res.message) 154 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 155 | t.truthy(res.metadata) 156 | const metadata = JSON.parse(res.metadata) 157 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 158 | t.deepEqual(metadata, expected) 159 | t.end() 160 | }) 161 | 162 | async.eachSeries(data, (d, asfn) => { 163 | call.write(d) 164 | _.delay(asfn, _.random(50, 150)) 165 | }, () => { 166 | call.end() 167 | }) 168 | }) 169 | 170 | test.cb('promised call service with metadata as plain object', t => { 171 | t.plan(5) 172 | const ts = new Date().getTime() 173 | const { call, res } = client.writeStuff({ requestId: 'bar-123', timestamp: ts }) 174 | res.then(res => { 175 | t.truthy(res) 176 | t.truthy(res.message) 177 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 178 | t.truthy(res.metadata) 179 | const metadata = JSON.parse(res.metadata) 180 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 181 | t.deepEqual(metadata, expected) 182 | t.end() 183 | }) 184 | 185 | async.eachSeries(data, (d, asfn) => { 186 | call.write(d) 187 | _.delay(asfn, _.random(50, 150)) 188 | }, () => { 189 | call.end() 190 | }) 191 | }) 192 | 193 | test.cb('call service with metadata as Metadata', t => { 194 | t.plan(6) 195 | const ts = new Date().getTime().toString() 196 | const reqMeta = new grpc.Metadata() 197 | reqMeta.add('requestId', 'bar-123') 198 | reqMeta.add('timestamp', ts) 199 | const call = client.writeStuff(reqMeta, (err, res) => { 200 | t.falsy(err) 201 | t.truthy(res) 202 | t.truthy(res.message) 203 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 204 | t.truthy(res.metadata) 205 | const metadata = JSON.parse(res.metadata) 206 | const expected = { requestid: 'bar-123', timestamp: ts } 207 | t.deepEqual(metadata, expected) 208 | t.end() 209 | }) 210 | 211 | async.eachSeries(data, (d, asfn) => { 212 | call.write(d) 213 | _.delay(asfn, _.random(50, 150)) 214 | }, () => { 215 | call.end() 216 | }) 217 | }) 218 | 219 | test.cb('call service with metadata as plain object and options object', t => { 220 | t.plan(6) 221 | const ts = new Date().getTime() 222 | const call = client.writeStuff({ requestId: 'bar-123', timestamp: ts }, { some: 'blah' }, (err, res) => { 223 | t.falsy(err) 224 | t.truthy(res) 225 | t.truthy(res.message) 226 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 227 | t.truthy(res.metadata) 228 | const metadata = JSON.parse(res.metadata) 229 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 230 | t.deepEqual(metadata, expected) 231 | t.end() 232 | }) 233 | 234 | async.eachSeries(data, (d, asfn) => { 235 | call.write(d) 236 | _.delay(asfn, _.random(50, 150)) 237 | }, () => { 238 | call.end() 239 | }) 240 | }) 241 | 242 | test.cb('promised call service with metadata as plain object and options object', t => { 243 | t.plan(5) 244 | const ts = new Date().getTime() 245 | const { call, res } = client.writeStuff({ requestId: 'bar-123', timestamp: ts }, { some: 'blah' }) 246 | res.then(res => { 247 | t.truthy(res) 248 | t.truthy(res.message) 249 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 250 | t.truthy(res.metadata) 251 | const metadata = JSON.parse(res.metadata) 252 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 253 | t.deepEqual(metadata, expected) 254 | t.end() 255 | }) 256 | 257 | async.eachSeries(data, (d, asfn) => { 258 | call.write(d) 259 | _.delay(asfn, _.random(50, 150)) 260 | }, () => { 261 | call.end() 262 | }) 263 | }) 264 | 265 | test.cb('call service with metadata as Metadata and options object', t => { 266 | t.plan(6) 267 | const ts = new Date().getTime().toString() 268 | const reqMeta = new grpc.Metadata() 269 | reqMeta.add('requestId', 'bar-123') 270 | reqMeta.add('timestamp', ts) 271 | const call = client.writeStuff(reqMeta, { some: 'blah' }, (err, res) => { 272 | t.falsy(err) 273 | t.truthy(res) 274 | t.truthy(res.message) 275 | t.is(res.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 276 | t.truthy(res.metadata) 277 | const metadata = JSON.parse(res.metadata) 278 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 279 | t.deepEqual(metadata, expected) 280 | t.end() 281 | }) 282 | 283 | async.eachSeries(data, (d, asfn) => { 284 | call.write(d) 285 | _.delay(asfn, _.random(50, 150)) 286 | }, () => { 287 | call.end() 288 | }) 289 | }) 290 | 291 | test.cb('Request API: call service using callback and just an argument', t => { 292 | t.plan(9) 293 | const req = new client.Request('writeStuff') 294 | const call = req.exec((err, res) => { 295 | t.falsy(err) 296 | const { response } = res 297 | t.truthy(res.response) 298 | t.truthy(res.call) 299 | t.falsy(res.metadata) 300 | t.falsy(res.status) 301 | t.truthy(response) 302 | t.truthy(response.message) 303 | t.falsy(response.metadata) 304 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 305 | t.end() 306 | }) 307 | 308 | async.eachSeries(data, (d, asfn) => { 309 | call.write(d) 310 | _.delay(asfn, _.random(50, 150)) 311 | }, () => { 312 | call.end() 313 | }) 314 | }) 315 | 316 | test('Request API: async call service using just an argument', async t => { 317 | t.plan(8) 318 | const req = new client.Request('writeStuff') 319 | const { call, res: p } = req.exec() 320 | 321 | async.eachSeries(data, (d, asfn) => { 322 | call.write(d) 323 | _.delay(asfn, _.random(50, 150)) 324 | }, () => { 325 | call.end() 326 | }) 327 | 328 | const res = await p 329 | const { response } = res 330 | t.truthy(res.response) 331 | t.truthy(res.call) 332 | t.falsy(res.metadata) 333 | t.falsy(res.status) 334 | t.truthy(response) 335 | t.truthy(response.message) 336 | t.falsy(response.metadata) 337 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 338 | }) 339 | 340 | test.cb('Request API: call service using callback with metadata as plain object', t => { 341 | t.plan(10) 342 | 343 | const ts = new Date().getTime() 344 | const req = new client.Request('writeStuff') 345 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 346 | 347 | const call = req.exec((err, res) => { 348 | t.falsy(err) 349 | const { response } = res 350 | t.truthy(res.response) 351 | t.truthy(res.call) 352 | t.falsy(res.metadata) 353 | t.falsy(res.status) 354 | t.truthy(response) 355 | t.truthy(response.message) 356 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 357 | t.truthy(response.metadata) 358 | const metadata = JSON.parse(response.metadata) 359 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 360 | t.deepEqual(metadata, expected) 361 | t.end() 362 | }) 363 | 364 | async.eachSeries(data, (d, asfn) => { 365 | call.write(d) 366 | _.delay(asfn, _.random(50, 150)) 367 | }, () => { 368 | call.end() 369 | }) 370 | }) 371 | 372 | test('Request API: async call service with metadata as plain object', async t => { 373 | t.plan(9) 374 | 375 | const ts = new Date().getTime() 376 | const req = new client.Request('writeStuff') 377 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 378 | 379 | const { call, res: p } = req.exec() 380 | 381 | async.eachSeries(data, (d, asfn) => { 382 | call.write(d) 383 | _.delay(asfn, _.random(50, 150)) 384 | }, () => { 385 | call.end() 386 | }) 387 | 388 | const res = await p 389 | const { response } = res 390 | t.truthy(res.response) 391 | t.truthy(res.call) 392 | t.falsy(res.metadata) 393 | t.falsy(res.status) 394 | t.truthy(response) 395 | t.truthy(response.message) 396 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 397 | t.truthy(response.metadata) 398 | const metadata = JSON.parse(response.metadata) 399 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 400 | t.deepEqual(metadata, expected) 401 | }) 402 | 403 | test('Request API: async call with metadata options', async t => { 404 | t.plan(12) 405 | 406 | const ts = new Date().getTime() 407 | const req = new client.Request('writeStuff') 408 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 409 | .withResponseMetadata(true) 410 | 411 | const { call, res: p } = req.exec() 412 | 413 | async.eachSeries(data, (d, asfn) => { 414 | call.write(d) 415 | _.delay(asfn, _.random(50, 150)) 416 | }, () => { 417 | call.end() 418 | }) 419 | 420 | const res = await p 421 | 422 | t.truthy(res.call) 423 | t.truthy(res.metadata) 424 | const md1 = res.metadata.getMap() 425 | const expectedMd = { headermd: 'headerValue' } 426 | t.is(md1.headermd, expectedMd.headermd) 427 | 428 | t.falsy(res.status) 429 | 430 | const { response } = res 431 | 432 | t.truthy(res.response) 433 | t.truthy(res.call) 434 | t.falsy(res.status) 435 | t.truthy(response) 436 | t.truthy(response.message) 437 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 438 | t.truthy(response.metadata) 439 | const metadata = JSON.parse(response.metadata) 440 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 441 | t.deepEqual(metadata, expected) 442 | }) 443 | 444 | test.cb('Request API: callback call with metadata options', t => { 445 | t.plan(13) 446 | 447 | const ts = new Date().getTime() 448 | const req = new client.Request('writeStuff') 449 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 450 | .withResponseMetadata(true) 451 | 452 | const call = req.exec((err, res) => { 453 | t.falsy(err) 454 | t.truthy(res.call) 455 | t.truthy(res.metadata) 456 | const md1 = res.metadata.getMap() 457 | const expectedMd = { headermd: 'headerValue' } 458 | t.is(md1.headermd, expectedMd.headermd) 459 | 460 | t.falsy(res.status) 461 | 462 | const { response } = res 463 | 464 | t.truthy(res.response) 465 | t.truthy(res.call) 466 | t.falsy(res.status) 467 | t.truthy(response) 468 | t.truthy(response.message) 469 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 470 | t.truthy(response.metadata) 471 | const metadata = JSON.parse(response.metadata) 472 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 473 | t.deepEqual(metadata, expected) 474 | t.end() 475 | }) 476 | 477 | async.eachSeries(data, (d, asfn) => { 478 | call.write(d) 479 | _.delay(asfn, _.random(50, 150)) 480 | }, () => { 481 | call.end() 482 | }) 483 | }) 484 | 485 | test('Request API: async call with status options', async t => { 486 | t.plan(15) 487 | 488 | const ts = new Date().getTime() 489 | const req = new client.Request('writeStuff') 490 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 491 | .withResponseMetadata(true) 492 | .withResponseStatus(true) 493 | 494 | const { call, res: p } = req.exec() 495 | 496 | async.eachSeries(data, (d, asfn) => { 497 | call.write(d) 498 | _.delay(asfn, _.random(50, 150)) 499 | }, () => { 500 | call.end() 501 | }) 502 | 503 | const res = await p 504 | 505 | t.truthy(res.call) 506 | t.truthy(res.metadata) 507 | const md1 = res.metadata.getMap() 508 | const expectedMd = { headermd: 'headerValue' } 509 | t.is(md1.headermd, expectedMd.headermd) 510 | 511 | t.truthy(res.status) 512 | t.is(res.status.code, 0) 513 | t.is(res.status.details, 'OK') 514 | t.truthy(res.status.metadata) 515 | const statusMD = res.status.metadata.getMap() 516 | const expectedStatusMD = { trailermd: 'trailerValue' } 517 | t.deepEqual(statusMD, expectedStatusMD) 518 | 519 | const { response } = res 520 | 521 | t.truthy(res.response) 522 | t.truthy(res.call) 523 | t.truthy(response) 524 | t.truthy(response.message) 525 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 526 | t.truthy(response.metadata) 527 | const metadata = JSON.parse(response.metadata) 528 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 529 | t.deepEqual(metadata, expected) 530 | }) 531 | 532 | test.cb('Request API: callback call with status options', t => { 533 | t.plan(16) 534 | 535 | const ts = new Date().getTime() 536 | const req = new client.Request('writeStuff') 537 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 538 | .withResponseMetadata(true) 539 | .withResponseStatus(true) 540 | 541 | const call = req.exec((err, res) => { 542 | t.falsy(err) 543 | 544 | t.truthy(res.call) 545 | t.truthy(res.metadata) 546 | const md1 = res.metadata.getMap() 547 | const expectedMd = { headermd: 'headerValue' } 548 | t.is(md1.headermd, expectedMd.headermd) 549 | 550 | t.truthy(res.status) 551 | t.is(res.status.code, 0) 552 | t.is(res.status.details, 'OK') 553 | t.truthy(res.status.metadata) 554 | const statusMD = res.status.metadata.getMap() 555 | const expectedStatusMD = { trailermd: 'trailerValue' } 556 | t.deepEqual(statusMD, expectedStatusMD) 557 | 558 | const { response } = res 559 | 560 | t.truthy(res.response) 561 | t.truthy(res.call) 562 | t.truthy(response) 563 | t.truthy(response.message) 564 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 565 | t.truthy(response.metadata) 566 | const metadata = JSON.parse(response.metadata) 567 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 568 | t.deepEqual(metadata, expected) 569 | 570 | t.end() 571 | }) 572 | 573 | async.eachSeries(data, (d, asfn) => { 574 | call.write(d) 575 | _.delay(asfn, _.random(50, 150)) 576 | }, () => { 577 | call.end() 578 | }) 579 | }) 580 | 581 | test('Request API: async call with metadata and status options', async t => { 582 | t.plan(14) 583 | 584 | const ts = new Date().getTime() 585 | const req = new client.Request('writeStuff') 586 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 587 | .withResponseStatus(true) 588 | 589 | const { call, res: p } = req.exec() 590 | 591 | async.eachSeries(data, (d, asfn) => { 592 | call.write(d) 593 | _.delay(asfn, _.random(50, 150)) 594 | }, () => { 595 | call.end() 596 | }) 597 | 598 | const res = await p 599 | 600 | t.truthy(res.call) 601 | t.falsy(res.metadata) 602 | t.truthy(res.status) 603 | t.is(res.status.code, 0) 604 | t.is(res.status.details, 'OK') 605 | t.truthy(res.status.metadata) 606 | const md1 = res.status.metadata.getMap() 607 | const expectedMd = { trailermd: 'trailerValue' } 608 | t.is(md1.headermd, expectedMd.headermd) 609 | 610 | const { response } = res 611 | 612 | t.truthy(res.response) 613 | t.truthy(res.call) 614 | t.truthy(response) 615 | t.truthy(response.message) 616 | t.is(response.message, '1 foo:2 bar:3 asd:4 qwe:5 rty:6 zxc:6') 617 | t.truthy(response.metadata) 618 | const metadata = JSON.parse(response.metadata) 619 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 620 | t.deepEqual(metadata, expected) 621 | }) 622 | 623 | test('Request API: expect to throw on unknown client method', t => { 624 | const error = t.throws(() => { 625 | const ts = new Date().getTime() 626 | const req = new client.Request('asdf') 627 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 628 | .withResponseStatus(true) 629 | 630 | req.exec() 631 | }) 632 | 633 | t.truthy(error) 634 | t.is(error.message, 'Invalid method: asdf') 635 | }) 636 | 637 | test.after.always.cb('guaranteed cleanup', t => { 638 | async.each(apps, (app, ascb) => app.tryShutdown(ascb), t.end) 639 | }) 640 | -------------------------------------------------------------------------------- /test/resstream.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const test = require('ava') 3 | const path = require('path') 4 | const async = require('async') 5 | const grpc = require('@grpc/grpc-js') 6 | 7 | const protoLoader = require('@grpc/proto-loader') 8 | 9 | const caller = require('../') 10 | 11 | const PROTO_PATH = path.resolve(__dirname, './protos/resstream.proto') 12 | const packageDefinition = protoLoader.loadSync(PROTO_PATH) 13 | const argProto = grpc.loadPackageDefinition(packageDefinition).argservice 14 | 15 | const apps = [] 16 | 17 | const data = [ 18 | { message: '1 foo' }, 19 | { message: '2 bar' }, 20 | { message: '3 asd' }, 21 | { message: '4 qwe' }, 22 | { message: '5 rty' }, 23 | { message: '6 zxc' } 24 | ] 25 | 26 | function getRandomInt (min, max) { 27 | return Math.floor(Math.random() * (max - min + 1)) + min 28 | } 29 | 30 | function getHost (port) { 31 | return '0.0.0.0:'.concat(port || getRandomInt(1000, 60000)) 32 | } 33 | 34 | const DYNAMIC_HOST = getHost() 35 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'ArgService') 36 | 37 | test.before('should dynamically create service', t => { 38 | function listStuff (call) { 39 | const reqMsg = call.request.message 40 | let meta = null 41 | if (call.metadata) { 42 | const reqMeta = call.metadata.getMap() 43 | if (reqMeta['user-agent']) { 44 | delete reqMeta['user-agent'] 45 | } 46 | if (!_.isEmpty(reqMeta)) { 47 | meta = JSON.stringify(reqMeta) 48 | } 49 | } 50 | 51 | async.eachSeries( 52 | data, 53 | (d, asfn) => { 54 | const ret = { message: d.message + ':' + reqMsg } 55 | if (meta) { 56 | ret.metadata = meta 57 | } 58 | call.write(ret) 59 | _.delay(asfn, _.random(50, 150)) 60 | }, 61 | () => { 62 | call.end() 63 | } 64 | ) 65 | } 66 | 67 | const server = new grpc.Server() 68 | server.addService(argProto.ArgService.service, { listStuff }) 69 | server.bindAsync(DYNAMIC_HOST, grpc.ServerCredentials.createInsecure(), err => { 70 | t.falsy(err) 71 | server.start() 72 | apps.push(server) 73 | }) 74 | }) 75 | 76 | test.cb('res stream call service using just an argument', t => { 77 | t.plan(1) 78 | let resData = [] 79 | const call = client.listStuff({ message: 'Hello' }) 80 | call.on('data', d => resData.push(d)) 81 | call.on('end', () => { 82 | resData = _.sortBy(resData, 'message') 83 | 84 | let expected = _.cloneDeep(data) 85 | expected = _.map(expected, d => { 86 | return { message: d.message + ':Hello' } 87 | }) 88 | 89 | t.deepEqual(resData, expected) 90 | t.end() 91 | }) 92 | }) 93 | 94 | test.cb('call service with metadata as plain object', t => { 95 | t.plan(1) 96 | let resData = [] 97 | const ts = new Date().getTime() 98 | const call = client.listStuff({ message: 'Hi' }, { requestId: 'bar-123', timestamp: ts }) 99 | call.on('data', d => { 100 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 101 | resData.push({ message: d.message, metadata }) 102 | }) 103 | 104 | call.on('end', () => { 105 | resData = _.sortBy(resData, 'message') 106 | 107 | let expected = _.cloneDeep(data) 108 | expected = _.map(expected, d => { 109 | d.message = d.message + ':Hi' 110 | d.metadata = { requestid: 'bar-123', timestamp: ts.toString() } 111 | return d 112 | }) 113 | 114 | t.deepEqual(resData, expected) 115 | t.end() 116 | }) 117 | }) 118 | 119 | test.cb('call service with metadata as Metadata', t => { 120 | t.plan(1) 121 | const ts = new Date().getTime().toString() 122 | const reqMeta = new grpc.Metadata() 123 | reqMeta.add('requestId', 'bar-123') 124 | reqMeta.add('timestamp', ts) 125 | 126 | let resData = [] 127 | 128 | const call = client.listStuff({ message: 'Yo' }, reqMeta) 129 | call.on('data', d => { 130 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 131 | resData.push({ message: d.message, metadata }) 132 | }) 133 | 134 | call.on('end', () => { 135 | resData = _.sortBy(resData, 'message') 136 | 137 | let expected = _.cloneDeep(data) 138 | expected = _.map(expected, d => { 139 | d.message = d.message + ':Yo' 140 | d.metadata = { requestid: 'bar-123', timestamp: ts } 141 | return d 142 | }) 143 | 144 | t.deepEqual(resData, expected) 145 | t.end() 146 | }) 147 | }) 148 | 149 | test.cb('call service with metadata as plain object and options object', t => { 150 | t.plan(1) 151 | let resData = [] 152 | const ts = new Date().getTime() 153 | const call = client.listStuff( 154 | { message: 'Hello' }, 155 | { requestId: 'bar-123', timestamp: ts }, 156 | { some: 'blah' } 157 | ) 158 | call.on('data', d => { 159 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 160 | resData.push({ message: d.message, metadata }) 161 | }) 162 | 163 | call.on('end', () => { 164 | resData = _.sortBy(resData, 'message') 165 | 166 | let expected = _.cloneDeep(data) 167 | expected = _.map(expected, d => { 168 | d.message = d.message + ':Hello' 169 | d.metadata = { requestid: 'bar-123', timestamp: ts.toString() } 170 | return d 171 | }) 172 | 173 | t.deepEqual(resData, expected) 174 | t.end() 175 | }) 176 | }) 177 | 178 | test.cb('call service with metadata as Metadata and options object', t => { 179 | t.plan(1) 180 | let resData = [] 181 | const ts = new Date().getTime().toString() 182 | const reqMeta = new grpc.Metadata() 183 | reqMeta.add('requestId', 'bar-123') 184 | reqMeta.add('timestamp', ts) 185 | const call = client.listStuff({ message: 'Hello' }, reqMeta, { some: 'blah' }) 186 | call.on('data', d => { 187 | const metadata = d.metadata ? JSON.parse(d.metadata) : '' 188 | resData.push({ message: d.message, metadata }) 189 | }) 190 | 191 | call.on('end', () => { 192 | resData = _.sortBy(resData, 'message') 193 | 194 | let expected = _.cloneDeep(data) 195 | expected = _.map(expected, d => { 196 | d.message = d.message + ':Hello' 197 | d.metadata = { requestid: 'bar-123', timestamp: ts } 198 | return d 199 | }) 200 | 201 | t.deepEqual(resData, expected) 202 | t.end() 203 | }) 204 | }) 205 | 206 | test('Request API: should fail due to unsupported call type', t => { 207 | const error = t.throws(() => { 208 | const req = new client.Request('listStuff', { message: 'Hello' }) 209 | 210 | req.exec() 211 | }) 212 | 213 | t.truthy(error) 214 | t.is(error.message, 'Invalid call: listStuff cannot be called using Request API') 215 | }) 216 | 217 | test.after.always.cb('guaranteed cleanup', t => { 218 | async.each(apps, (app, ascb) => app.tryShutdown(ascb), t.end) 219 | }) 220 | -------------------------------------------------------------------------------- /test/retry.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const test = require('ava') 3 | const path = require('path') 4 | const async = require('async') 5 | const grpc = require('@grpc/grpc-js') 6 | 7 | const protoLoader = require('@grpc/proto-loader') 8 | 9 | const caller = require('../') 10 | 11 | const PROTO_PATH = path.resolve(__dirname, './protos/reqres.proto') 12 | const packageDefinition = protoLoader.loadSync(PROTO_PATH) 13 | const argProto = grpc.loadPackageDefinition(packageDefinition).argservice 14 | 15 | const apps = [] 16 | 17 | function getRandomInt (min, max) { 18 | return Math.floor(Math.random() * (max - min + 1)) + min 19 | } 20 | 21 | function getHost (port) { 22 | return '0.0.0.0:'.concat(port || getRandomInt(1000, 60000)) 23 | } 24 | 25 | const DYNAMIC_HOST = getHost() 26 | const client = caller(DYNAMIC_HOST, PROTO_PATH, 'ArgService') 27 | 28 | let callCounter = 0 29 | 30 | test.before('should dynamically create service', t => { 31 | function doSomething (call, callback) { 32 | callCounter++ 33 | const ret = { message: call.request.message } 34 | 35 | const md = new grpc.Metadata() 36 | md.set('headerMD', 'headerValue') 37 | call.sendMetadata(md) 38 | 39 | if (call.metadata) { 40 | const meta = call.metadata.getMap() 41 | if (meta['user-agent']) { 42 | delete meta['user-agent'] 43 | } 44 | if (!_.isEmpty(meta)) { 45 | ret.metadata = JSON.stringify(meta) 46 | } 47 | } 48 | 49 | const md2 = new grpc.Metadata() 50 | md2.set('trailerMD', 'trailerValue') 51 | 52 | if (ret.message.toLowerCase() === 'bad' && callCounter < 3) { 53 | return callback(new Error('Bad Request'), null, md2) 54 | } 55 | 56 | callback(null, ret, md2) 57 | } 58 | 59 | const server = new grpc.Server() 60 | server.addService(argProto.ArgService.service, { doSomething }) 61 | server.bindAsync(DYNAMIC_HOST, grpc.ServerCredentials.createInsecure(), err => { 62 | t.falsy(err) 63 | server.start() 64 | apps.push(server) 65 | }) 66 | }) 67 | 68 | test.serial.cb('Retry: call service using retry option and callback', t => { 69 | t.plan(6) 70 | 71 | callCounter = 0 72 | 73 | client.doSomething({ message: 'Bad' }, {}, { retry: 5 }, (err, response) => { 74 | t.falsy(err) 75 | t.truthy(response) 76 | t.truthy(response.message) 77 | t.falsy(response.metadata) 78 | t.is(response.message, 'Bad') 79 | t.is(callCounter, 3) 80 | t.end() 81 | }) 82 | }) 83 | 84 | test.serial('Retry: async call service using retry option', async t => { 85 | t.plan(5) 86 | 87 | callCounter = 0 88 | 89 | const response = await client.doSomething({ message: 'Bad' }, {}, { retry: 5 }) 90 | t.is(callCounter, 3) 91 | t.truthy(response) 92 | t.truthy(response.message) 93 | t.falsy(response.metadata) 94 | t.is(response.message, 'Bad') 95 | }) 96 | 97 | test.serial.cb('Request API with retry: call service using callback and just an argument', t => { 98 | t.plan(10) 99 | 100 | callCounter = 0 101 | 102 | const req = new client 103 | .Request('doSomething', { message: 'Bad' }) 104 | .withRetry(5) 105 | 106 | req.exec((err, res) => { 107 | t.falsy(err) 108 | t.is(callCounter, 3) 109 | const { response } = res 110 | t.truthy(res.response) 111 | t.truthy(res.call) 112 | t.falsy(res.metadata) 113 | t.falsy(res.status) 114 | t.truthy(response) 115 | t.truthy(response.message) 116 | t.falsy(response.metadata) 117 | t.is(response.message, 'Bad') 118 | t.end() 119 | }) 120 | }) 121 | 122 | test.serial('Request API with retry: call service using async with just an argument', async t => { 123 | t.plan(9) 124 | 125 | callCounter = 0 126 | 127 | const req = new client 128 | .Request('doSomething', { message: 'Bad' }) 129 | .withRetry(5) 130 | 131 | const res = await req.exec() 132 | 133 | t.is(callCounter, 3) 134 | 135 | const { response } = res 136 | t.truthy(res.response) 137 | t.truthy(res.call) 138 | t.falsy(res.metadata) 139 | t.falsy(res.status) 140 | t.truthy(response) 141 | t.truthy(response.message) 142 | t.falsy(response.metadata) 143 | t.is(response.message, 'Bad') 144 | }) 145 | 146 | test.serial('Request API with retry: call service using async with metadata and options', async t => { 147 | t.plan(13) 148 | 149 | callCounter = 0 150 | 151 | const ts = new Date().getTime() 152 | 153 | const req = new client 154 | .Request('doSomething', { message: 'Bad' }) 155 | .withMetadata({ requestId: 'bar-123', timestamp: ts }) 156 | .withResponseMetadata(true) 157 | .withResponseStatus(true) 158 | .withRetry(5) 159 | 160 | const res = await req.exec() 161 | 162 | t.is(callCounter, 3) 163 | 164 | t.truthy(res.metadata) 165 | const md1 = res.metadata.getMap() 166 | const expectedMd = { headermd: 'headerValue' } 167 | t.is(md1.headermd, expectedMd.headermd) 168 | 169 | t.truthy(res.status) 170 | t.is(res.status.code, 0) 171 | t.is(res.status.details, 'OK') 172 | t.truthy(res.status.metadata) 173 | const md2 = res.status.metadata.getMap() 174 | const expectedMd2 = { trailermd: 'trailerValue' } 175 | t.deepEqual(md2, expectedMd2) 176 | 177 | const { response } = res 178 | t.truthy(response) 179 | t.truthy(response.message) 180 | t.is(response.message, 'Bad') 181 | t.truthy(response.metadata) 182 | const metadata = JSON.parse(response.metadata) 183 | const expected = { requestid: 'bar-123', timestamp: ts.toString() } 184 | t.deepEqual(metadata, expected) 185 | }) 186 | 187 | test.after.always.cb('guaranteed cleanup', t => { 188 | async.each(apps, (app, ascb) => app.tryShutdown(ascb), t.end) 189 | }) 190 | -------------------------------------------------------------------------------- /test/static/helloworld_grpc_pb.js: -------------------------------------------------------------------------------- 1 | // GENERATED CODE -- DO NOT EDIT! 2 | 3 | // Original file comments: 4 | // Copyright 2015 gRPC authors. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 'use strict'; 19 | var grpc = require('@grpc/grpc-js'); 20 | var helloworld_pb = require('./helloworld_pb.js'); 21 | 22 | function serialize_helloworld_HelloReply(arg) { 23 | if (!(arg instanceof helloworld_pb.HelloReply)) { 24 | throw new Error('Expected argument of type helloworld.HelloReply'); 25 | } 26 | return Buffer.from(arg.serializeBinary()); 27 | } 28 | 29 | function deserialize_helloworld_HelloReply(buffer_arg) { 30 | return helloworld_pb.HelloReply.deserializeBinary(new Uint8Array(buffer_arg)); 31 | } 32 | 33 | function serialize_helloworld_HelloRequest(arg) { 34 | if (!(arg instanceof helloworld_pb.HelloRequest)) { 35 | throw new Error('Expected argument of type helloworld.HelloRequest'); 36 | } 37 | return Buffer.from(arg.serializeBinary()); 38 | } 39 | 40 | function deserialize_helloworld_HelloRequest(buffer_arg) { 41 | return helloworld_pb.HelloRequest.deserializeBinary(new Uint8Array(buffer_arg)); 42 | } 43 | 44 | 45 | // The greeting service definition. 46 | var GreeterService = exports.GreeterService = { 47 | // Sends a greeting 48 | sayHello: { 49 | path: '/helloworld.Greeter/SayHello', 50 | requestStream: false, 51 | responseStream: false, 52 | requestType: helloworld_pb.HelloRequest, 53 | responseType: helloworld_pb.HelloReply, 54 | requestSerialize: serialize_helloworld_HelloRequest, 55 | requestDeserialize: deserialize_helloworld_HelloRequest, 56 | responseSerialize: serialize_helloworld_HelloReply, 57 | responseDeserialize: deserialize_helloworld_HelloReply, 58 | }, 59 | }; 60 | 61 | exports.GreeterClient = grpc.makeGenericClientConstructor(GreeterService); -------------------------------------------------------------------------------- /test/static/helloworld_pb.js: -------------------------------------------------------------------------------- 1 | // source: helloworld.proto 2 | /** 3 | * @fileoverview 4 | * @enhanceable 5 | * @suppress {messageConventions} JS Compiler reports an error if a variable or 6 | * field starts with 'MSG_' and isn't a translatable message. 7 | * @public 8 | */ 9 | // GENERATED CODE -- DO NOT EDIT! 10 | 11 | var jspb = require('google-protobuf'); 12 | var goog = jspb; 13 | var global = Function('return this')(); 14 | 15 | goog.exportSymbol('proto.helloworld.HelloReply', null, global); 16 | goog.exportSymbol('proto.helloworld.HelloRequest', null, global); 17 | /** 18 | * Generated by JsPbCodeGenerator. 19 | * @param {Array=} opt_data Optional initial data array, typically from a 20 | * server response, or constructed directly in Javascript. The array is used 21 | * in place and becomes part of the constructed object. It is not cloned. 22 | * If no data is provided, the constructed object will be empty, but still 23 | * valid. 24 | * @extends {jspb.Message} 25 | * @constructor 26 | */ 27 | proto.helloworld.HelloRequest = function(opt_data) { 28 | jspb.Message.initialize(this, opt_data, 0, -1, null, null); 29 | }; 30 | goog.inherits(proto.helloworld.HelloRequest, jspb.Message); 31 | if (goog.DEBUG && !COMPILED) { 32 | /** 33 | * @public 34 | * @override 35 | */ 36 | proto.helloworld.HelloRequest.displayName = 'proto.helloworld.HelloRequest'; 37 | } 38 | /** 39 | * Generated by JsPbCodeGenerator. 40 | * @param {Array=} opt_data Optional initial data array, typically from a 41 | * server response, or constructed directly in Javascript. The array is used 42 | * in place and becomes part of the constructed object. It is not cloned. 43 | * If no data is provided, the constructed object will be empty, but still 44 | * valid. 45 | * @extends {jspb.Message} 46 | * @constructor 47 | */ 48 | proto.helloworld.HelloReply = function(opt_data) { 49 | jspb.Message.initialize(this, opt_data, 0, -1, null, null); 50 | }; 51 | goog.inherits(proto.helloworld.HelloReply, jspb.Message); 52 | if (goog.DEBUG && !COMPILED) { 53 | /** 54 | * @public 55 | * @override 56 | */ 57 | proto.helloworld.HelloReply.displayName = 'proto.helloworld.HelloReply'; 58 | } 59 | 60 | 61 | 62 | if (jspb.Message.GENERATE_TO_OBJECT) { 63 | /** 64 | * Creates an object representation of this proto. 65 | * Field names that are reserved in JavaScript and will be renamed to pb_name. 66 | * Optional fields that are not set will be set to undefined. 67 | * To access a reserved field use, foo.pb_, eg, foo.pb_default. 68 | * For the list of reserved names please see: 69 | * net/proto2/compiler/js/internal/generator.cc#kKeyword. 70 | * @param {boolean=} opt_includeInstance Deprecated. whether to include the 71 | * JSPB instance for transitional soy proto support: 72 | * http://goto/soy-param-migration 73 | * @return {!Object} 74 | */ 75 | proto.helloworld.HelloRequest.prototype.toObject = function(opt_includeInstance) { 76 | return proto.helloworld.HelloRequest.toObject(opt_includeInstance, this); 77 | }; 78 | 79 | 80 | /** 81 | * Static version of the {@see toObject} method. 82 | * @param {boolean|undefined} includeInstance Deprecated. Whether to include 83 | * the JSPB instance for transitional soy proto support: 84 | * http://goto/soy-param-migration 85 | * @param {!proto.helloworld.HelloRequest} msg The msg instance to transform. 86 | * @return {!Object} 87 | * @suppress {unusedLocalVariables} f is only used for nested messages 88 | */ 89 | proto.helloworld.HelloRequest.toObject = function(includeInstance, msg) { 90 | var f, obj = { 91 | name: jspb.Message.getFieldWithDefault(msg, 1, "") 92 | }; 93 | 94 | if (includeInstance) { 95 | obj.$jspbMessageInstance = msg; 96 | } 97 | return obj; 98 | }; 99 | } 100 | 101 | 102 | /** 103 | * Deserializes binary data (in protobuf wire format). 104 | * @param {jspb.ByteSource} bytes The bytes to deserialize. 105 | * @return {!proto.helloworld.HelloRequest} 106 | */ 107 | proto.helloworld.HelloRequest.deserializeBinary = function(bytes) { 108 | var reader = new jspb.BinaryReader(bytes); 109 | var msg = new proto.helloworld.HelloRequest; 110 | return proto.helloworld.HelloRequest.deserializeBinaryFromReader(msg, reader); 111 | }; 112 | 113 | 114 | /** 115 | * Deserializes binary data (in protobuf wire format) from the 116 | * given reader into the given message object. 117 | * @param {!proto.helloworld.HelloRequest} msg The message object to deserialize into. 118 | * @param {!jspb.BinaryReader} reader The BinaryReader to use. 119 | * @return {!proto.helloworld.HelloRequest} 120 | */ 121 | proto.helloworld.HelloRequest.deserializeBinaryFromReader = function(msg, reader) { 122 | while (reader.nextField()) { 123 | if (reader.isEndGroup()) { 124 | break; 125 | } 126 | var field = reader.getFieldNumber(); 127 | switch (field) { 128 | case 1: 129 | var value = /** @type {string} */ (reader.readString()); 130 | msg.setName(value); 131 | break; 132 | default: 133 | reader.skipField(); 134 | break; 135 | } 136 | } 137 | return msg; 138 | }; 139 | 140 | 141 | /** 142 | * Serializes the message to binary data (in protobuf wire format). 143 | * @return {!Uint8Array} 144 | */ 145 | proto.helloworld.HelloRequest.prototype.serializeBinary = function() { 146 | var writer = new jspb.BinaryWriter(); 147 | proto.helloworld.HelloRequest.serializeBinaryToWriter(this, writer); 148 | return writer.getResultBuffer(); 149 | }; 150 | 151 | 152 | /** 153 | * Serializes the given message to binary data (in protobuf wire 154 | * format), writing to the given BinaryWriter. 155 | * @param {!proto.helloworld.HelloRequest} message 156 | * @param {!jspb.BinaryWriter} writer 157 | * @suppress {unusedLocalVariables} f is only used for nested messages 158 | */ 159 | proto.helloworld.HelloRequest.serializeBinaryToWriter = function(message, writer) { 160 | var f = undefined; 161 | f = message.getName(); 162 | if (f.length > 0) { 163 | writer.writeString( 164 | 1, 165 | f 166 | ); 167 | } 168 | }; 169 | 170 | 171 | /** 172 | * optional string name = 1; 173 | * @return {string} 174 | */ 175 | proto.helloworld.HelloRequest.prototype.getName = function() { 176 | return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); 177 | }; 178 | 179 | 180 | /** 181 | * @param {string} value 182 | * @return {!proto.helloworld.HelloRequest} returns this 183 | */ 184 | proto.helloworld.HelloRequest.prototype.setName = function(value) { 185 | return jspb.Message.setProto3StringField(this, 1, value); 186 | }; 187 | 188 | 189 | 190 | 191 | 192 | if (jspb.Message.GENERATE_TO_OBJECT) { 193 | /** 194 | * Creates an object representation of this proto. 195 | * Field names that are reserved in JavaScript and will be renamed to pb_name. 196 | * Optional fields that are not set will be set to undefined. 197 | * To access a reserved field use, foo.pb_, eg, foo.pb_default. 198 | * For the list of reserved names please see: 199 | * net/proto2/compiler/js/internal/generator.cc#kKeyword. 200 | * @param {boolean=} opt_includeInstance Deprecated. whether to include the 201 | * JSPB instance for transitional soy proto support: 202 | * http://goto/soy-param-migration 203 | * @return {!Object} 204 | */ 205 | proto.helloworld.HelloReply.prototype.toObject = function(opt_includeInstance) { 206 | return proto.helloworld.HelloReply.toObject(opt_includeInstance, this); 207 | }; 208 | 209 | 210 | /** 211 | * Static version of the {@see toObject} method. 212 | * @param {boolean|undefined} includeInstance Deprecated. Whether to include 213 | * the JSPB instance for transitional soy proto support: 214 | * http://goto/soy-param-migration 215 | * @param {!proto.helloworld.HelloReply} msg The msg instance to transform. 216 | * @return {!Object} 217 | * @suppress {unusedLocalVariables} f is only used for nested messages 218 | */ 219 | proto.helloworld.HelloReply.toObject = function(includeInstance, msg) { 220 | var f, obj = { 221 | message: jspb.Message.getFieldWithDefault(msg, 1, "") 222 | }; 223 | 224 | if (includeInstance) { 225 | obj.$jspbMessageInstance = msg; 226 | } 227 | return obj; 228 | }; 229 | } 230 | 231 | 232 | /** 233 | * Deserializes binary data (in protobuf wire format). 234 | * @param {jspb.ByteSource} bytes The bytes to deserialize. 235 | * @return {!proto.helloworld.HelloReply} 236 | */ 237 | proto.helloworld.HelloReply.deserializeBinary = function(bytes) { 238 | var reader = new jspb.BinaryReader(bytes); 239 | var msg = new proto.helloworld.HelloReply; 240 | return proto.helloworld.HelloReply.deserializeBinaryFromReader(msg, reader); 241 | }; 242 | 243 | 244 | /** 245 | * Deserializes binary data (in protobuf wire format) from the 246 | * given reader into the given message object. 247 | * @param {!proto.helloworld.HelloReply} msg The message object to deserialize into. 248 | * @param {!jspb.BinaryReader} reader The BinaryReader to use. 249 | * @return {!proto.helloworld.HelloReply} 250 | */ 251 | proto.helloworld.HelloReply.deserializeBinaryFromReader = function(msg, reader) { 252 | while (reader.nextField()) { 253 | if (reader.isEndGroup()) { 254 | break; 255 | } 256 | var field = reader.getFieldNumber(); 257 | switch (field) { 258 | case 1: 259 | var value = /** @type {string} */ (reader.readString()); 260 | msg.setMessage(value); 261 | break; 262 | default: 263 | reader.skipField(); 264 | break; 265 | } 266 | } 267 | return msg; 268 | }; 269 | 270 | 271 | /** 272 | * Serializes the message to binary data (in protobuf wire format). 273 | * @return {!Uint8Array} 274 | */ 275 | proto.helloworld.HelloReply.prototype.serializeBinary = function() { 276 | var writer = new jspb.BinaryWriter(); 277 | proto.helloworld.HelloReply.serializeBinaryToWriter(this, writer); 278 | return writer.getResultBuffer(); 279 | }; 280 | 281 | 282 | /** 283 | * Serializes the given message to binary data (in protobuf wire 284 | * format), writing to the given BinaryWriter. 285 | * @param {!proto.helloworld.HelloReply} message 286 | * @param {!jspb.BinaryWriter} writer 287 | * @suppress {unusedLocalVariables} f is only used for nested messages 288 | */ 289 | proto.helloworld.HelloReply.serializeBinaryToWriter = function(message, writer) { 290 | var f = undefined; 291 | f = message.getMessage(); 292 | if (f.length > 0) { 293 | writer.writeString( 294 | 1, 295 | f 296 | ); 297 | } 298 | }; 299 | 300 | 301 | /** 302 | * optional string message = 1; 303 | * @return {string} 304 | */ 305 | proto.helloworld.HelloReply.prototype.getMessage = function() { 306 | return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); 307 | }; 308 | 309 | 310 | /** 311 | * @param {string} value 312 | * @return {!proto.helloworld.HelloReply} returns this 313 | */ 314 | proto.helloworld.HelloReply.prototype.setMessage = function(value) { 315 | return jspb.Message.setProto3StringField(this, 1, value); 316 | }; 317 | 318 | 319 | goog.object.extend(exports, proto.helloworld); --------------------------------------------------------------------------------