grpc_tools から生成した gRPC クライアントを promisify してみた。

# 前提条件

```shell
% node --version
v12.13.0
```

```shell
% npm --version
6.13.2
```

# 目的

最近、`grpc_tools` と `grpc_tools_node_protoc_ts` を併せて Typescript の型ファイルと node.js の gRPC のクライアントを生成する機会がありました。生成される GRPC クライアントなのですが、Node.js にありがちな callback にて行う非同期処理です。そのため、GraphQL のリゾルバ等と組み合わせて使用する場合、非常に使い勝手が悪いです。

今回は生成された gRPC クライアントの関数を promisify します。

# gRPC クライアントの生成

例えば、次のような protocol buffer から pb ファイルを生成することを考えます。

```text:user.proto
syntax = "proto3";

package user;

option go_package = "v1";

service UserService {
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}

message CreateUserRequest {
    int64 id = 1;
    string name = 2;
}

message CreateUserResponse {
    int64 id = 1;
    string name = 2;
}

```

`grpc_tools_node_protoc` コマンドから pb ファイルを生成します。

```shell
dist='src/grpc/generated'; \
grpc_tools_node_protoc \
  --js_out=import_style=commonjs,binary:${dist} \
  --ts_out=${dist} \
  --grpc_out=${dist} \
  -I ./proto
  ./proto/user.proto
```

生成された型ファイルの一部を抜粋します。

```typescript:user_grpc_pb.d.ts
export class UserServiceClient extends grpc.Client implements IUserServiceClient {
    constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);
    public createUser(request: user_pb.CreateUserRequest, callback: (error: grpc.ServiceError | null, response: user_pb.CreateUserResponse) => void): grpc.ClientUnaryCall;
    public createUser(request: user_pb.CreateUserRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: user_pb.CreateUserResponse) => void): grpc.ClientUnaryCall;
    public createUser(request: user_pb.CreateUserRequest, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: user_pb.CreateUserResponse) => void): grpc.ClientUnaryCall;
}
```

`createUser` 関数の最後の引数がそえぞれ callback となっていることがわかります。

# クライアントの実装

## Promise を実装する。

まずは自力で Promise を実装します。

```typescript:client.ts
import { UserServiceClient } from './generated/user_grpc_pb';
import { credentials } from 'grpc';
import { CreateUserRequest, CreateUserResponse } from './generated/user_pb';

export const createClient = (url: string) => (
  request: CreateUserRequest
): Promise<CreateUserResponse> => {
  const client = new UserServiceClient(url, credentials.createInsecure());

  return new Promise((resolve, reject) => {
    client.createUser(request, (err, response) => {
      err === null ? resolve(response) : reject(err);
    })
  });
};
```

コレでも良いのですが、gRPC のエンドポイントが増えるたびに実装を行うのはなかなかツライです。

## util.promisify を利用する。

次に Node.js の util.promisify を利用することを考えてみます。`UserServiceClient` クラスのメソッドを `promisify` しているため、`bind` 関数により `this` を束縛しないとエラーが発生することに注意してください。

```typescript:client.ts
import { UserServiceClient } from './generated/user_grpc_pb';
import { credentials } from 'grpc';
import { CreateUserRequest, CreateUserResponse } from './generated/user_pb';
import { promisify } from 'util'

export const createClient = (url: string) => (
  request: CreateUserRequest
): Promise<CreateUserResponse> => {
  const client = new UserServiceClient(url, credentials.createInsecure());
  return promisify<CreateUserRequest, CreateUserResponse>(client.createUser).bind(client)(request)
};
```

エラーの有無に起因する分岐処理はなくなりましたが、ジェネリクスを指定する必要があります。

## Bluebird.js を利用する。

Promise の実装である Bluebird.js を使用すると次のようになります。

```typescript:client.ts
import { UserServiceClient } from './generated/user_grpc_pb';
import { credentials } from 'grpc';
import { CreateUserRequest, CreateUserResponse } from './generated/user_pb';
import { promisify } from 'bluebird'

export const createClient = (url: string) => (
  request: CreateUserRequest
): Promise<CreateUserResponse> => {
  const client = new UserServiceClient(url, credentials.createInsecure());
  return promisify(client.createUser, { context: client })(request)
};
```

ジェネリクスがなくなり、`this` の束縛も含めてシンプルになった印象です。`promisify` により生成される関数の戻り値の型が `Bluebird<unknown>` から `Promise<CreateUserResponse>` へ暗黙的にキャストが行われている点が少し気になりますが...

# 所感

今の所、Bluebird.js を利用した promisify がベターだと感じています。そもそものクライアントの実装をなんとかしてほしいところではあります。他の良い方法があれば是非とも教えて頂きたいです。