├── .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 | [](https://www.npmjs.com/package/grpc-caller)
6 | [](https://travis-ci.org/bojand/grpc-caller)
7 | [](https://standardjs.com)
8 | [](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
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 | [](https://www.npmjs.com/package/grpc-caller)
6 | [](https://travis-ci.org/bojand/grpc-caller)
7 | [](https://standardjs.com)
8 | [](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_