LoginSignup
4
0

More than 3 years have passed since last update.

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

Posted at

前提条件

% node --version
v12.13.0
% npm --version
6.13.2

目的

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

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

gRPC クライアントの生成

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

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 ファイルを生成します。

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

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

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 を実装します。

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 を束縛しないとエラーが発生することに注意してください。

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 を使用すると次のようになります。

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 がベターだと感じています。そもそものクライアントの実装をなんとかしてほしいところではあります。他の良い方法があれば是非とも教えて頂きたいです。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0