前提条件
% node --version
v12.13.0
% 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 ファイルを生成することを考えます。
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
生成された型ファイルの一部を抜粋します。
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 を実装します。
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
を束縛しないとエラーが発生することに注意してください。
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 を使用すると次のようになります。
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 がベターだと感じています。そもそものクライアントの実装をなんとかしてほしいところではあります。他の良い方法があれば是非とも教えて頂きたいです。