🛵 Universal, extendable, typesafe gRPC/gRPC-Web client for node used in ezy.
Warning
This lib is not production ready until it is merged to main ezy project.
npm i @getezy/grpc-client
This library was designed to be extendable as much as possible.
To start you need GrpcClient
, Loader
and Protocol
.
Use GrpcClientFactory
for creating GrpcClient
instance.
GrpcClientFactory.create(loader: AbstractLoader, protocol: AbstractProtocol): Promise<GrpcClient>
import { GrpcClientFactory, GrpcProtocol, ProtobufLoader } from '@getezy/grpc-client';
import * as path from 'node:path';
const client = await GrpcClientFactory.create(
new ProtobufLoader(path.join(__dirname, '../proto/main.proto')),
new GrpcProtocol({ address: '10.10.10.10' })
);
const payload = { id: '21443e83-d6ab-45b7-9afd-65b2e0ee8957' };
const metadata = { 'x-access-token': 'token' };
const response = await client.invokeUnaryRequest<Request, Response>(
{ service: 'simple_package.v1.SimpleService', method: 'SimpleUnaryRequest' },
payload,
metadata
);
GrpcClient
has public methods to query gRPC server:
class GrpcClient<MetadataValue, Metadata> {
public invokeUnaryRequest<Request, Response>(
options: GrpcRequestOptions,
request: Request,
metadata?: Record<string, MetadataValue>
): Promise<GrpcResponse<Response>>;
public invokeClientStreamingRequest<Request, Response>(
options: GrpcRequestOptions,
metadata?: Record<string, MetadataValue>
): ClientStream<Request, Response>;
public invokeServerStreamingRequest<Request, Response>(
options: GrpcRequestOptions,
payload: Request,
metadata?: Record<string, MetadataValue>
): ServerStream<Response>;
public invokeBidirectionalStreamingRequest<Request, Response>(
options: GrpcRequestOptions,
metadata?: Record<string, MetadataValue>
): BidirectionalStream<Request, Response>;
}
The first argument in each method defines the query options - service
and method
name.
interface GrpcRequestOptions {
service: string;
method: string;
}
Response
type in methods are wrapped with GrpcResponse
type.
interface GrpcErrorResponseValue {
details?: string;
metadata?: Record<string, unknown>;
}
type GrpcResponseValue = Record<string, unknown>;
interface GrpcResponse<Response extends GrpcResponseValue = GrpcResponseValue> {
/**
* Status code of gRPC request
*/
code: GrpcStatus;
/**
* For unary requests - query execution time in milliseconds.
* For streaming requests - receiving response actual time in utc.
*/
timestamp: number;
data: Response | GrpcErrorResponseValue;
}
const response = await client.invokeUnaryRequest(
{ service: 'simple_package.v1.SimpleService', method: 'SimpleUnaryRequest' },
{ id: '21443e83-d6ab-45b7-9afd-65b2e0ee8957' }
);
Note
If error occured this method will not through error. You can handle this by checking status code in response.
import { GrpcStatus } from '@getezy/grpc-client';
const response = await client.invokeUnaryRequest(
{ service: 'simple_package.v1.SimpleService', method: 'SimpleUnaryRequest' },
{ id: '21443e83-d6ab-45b7-9afd-65b2e0ee8957' }
);
if (response.code !== GrpcStatus.OK) {
// do something
}
const stream = client.invokeClientStreamingRequest({
service: 'simple_package.v1.SimpleService',
method: 'SimpleClientStreamRequest',
});
stream.on('response', (response) => {});
stream.on('error', (error) => {});
stream.write({ id: '21443e83-d6ab-45b7-9afd-65b2e0ee8957' });
stream.end();
// If you want to cancel stream from the client do
stream.cancel()
ClientStream
extended from EventEmitter
.
stream.on('response', (response: GrpcResponse<Response>) => {})
Sibscribe on server response.
stream.on('error', (error: GrpcResponse<Response>) => {})
Subscribe on server error.
stream.write(payload: Request)
Send payload to the stream.
stream.cancel()
Cancel the stream.
stream.end()
End the stream.
const stream = client.invokeServerStreamingRequest(
{
service: 'simple_package.v1.SimpleService',
method: 'SimpleServerStreamRequest',
},
{ id: '21443e83-d6ab-45b7-9afd-65b2e0ee8957' },
);
stream.on('response', (response) => {});
stream.on('error', (error) => {});
stream.on('end', () => {});
// If you want to cancel stream from the client do
stream.cancel()
ServerStream
extended from EventEmitter
.
stream.on('response', (response: GrpcResponse<Response>) => {})
Sibscribe on server response.
stream.on('error', (error: GrpcResponse<Response>) => {})
Subscribe on server error.
stream.cancel()
Cancel the stream.
const stream = client.invokeBidirectionalStreamingRequest({
service: 'simple_package.v1.SimpleService',
method: 'SimpleBidirectionalStreamRequest',
});
stream.on('response', (response) => {});
stream.on('error', (error) => {});
stream.on('end-server-stream', () => {})
stream.write({ id: '21443e83-d6ab-45b7-9afd-65b2e0ee8957' });
stream.end();
// If you want to cancel stream from the client do
stream.cancel()
BidirectionalStream
extended from EventEmitter
.
stream.on('response', (response: GrpcResponse<Response>) => {})
Sibscribe on server response.
stream.on('error', (error: GrpcResponse<Response>) => {})
Subscribe on server error.
stream.on('end-server-stream', () => {})
Subscribe on end server stream.
stream.write(payload: Request)
Send payload to the stream.
stream.cancel()
Cancel the stream.
stream.end()
End the client stream.
Read this article to understand how to configure TLS for gRPC and gRPC-Web server.
There are three connection types:
- Insecure — all data transmitted without encryption.
- Server-Side TLS — browser like encryption, where only the server provides TLS certificate to the client.
- Mutual TLS — most secure, both the server and the client provides their certificates to each other.
Note
Each protocol accepts TLS options. If TLS options are not specifiedInsecure
connection will be used by default.
import { GrpcProtocol, GrpcTlsType } from '@getezy/grpc-client';
const protocol = new GrpcProtocol({
address: '10.10.10.10',
tls: {
type: GrpcTlsType.INSECURE,
},
});
import * as path from 'node:path';
import { GrpcProtocol, GrpcTlsType } from '@getezy/grpc-client';
const protocol = new GrpcProtocol({
address: '10.10.10.10',
tls: {
tls: {
type: GrpcTlsType.SERVER_SIDE,
rootCertificatePath: path.join(__dirname, '../certs/ca-cert.pem'),
},
},
});
Note
rootCertificatePath - is optional, usually used if your server has self-signed CA
import * as path from 'node:path';
import { GrpcProtocol, GrpcTlsType } from '@getezy/grpc-client';
const protocol = new GrpcProtocol({
address: '10.10.10.10',
tls: {
type: GrpcTlsType.MUTUAL,
rootCertificatePath: path.join(__dirname, '../certs/ca-cert.pem'),
clientCertificatePath: path.join(__dirname, '../certs/client-cert.pem'),
clientKeyPath: path.join(__dirname, '../certs/client-key.pem'),
},
});
Note
rootCertificatePath - is optional, usually used if your server has self-signed CA
Loader
- is the strategy defines how to load gRPC package definitions.
Uses @grpc/proto-loader for load protobuf definition from the giving path.
new ProtobufLoader(path: string, [options])
import { ProtobufLoader } from '@getezy/grpc-client';
import * as path from 'node:path';
const loader = new ProtobufLoader(
path.join(__dirname, '../proto/main.proto'),
{
defaults: false,
}
);
await loader.load();
Refer to @grpc/proto-loader
documentation to see available options.
Default options are:
{
// Preserve field names. The default
// is to change them to camel case.
keepCase: true,
// Set default values on output objects.
// Defaults to false.
defaults: true,
// A list of search paths for imported .proto files.
includeDirs: [],
// The type to use to represent long values.
// Defaults to a Long object type.
longs: String,
}
Loader by reflection API is coming soon.
You can write custom loader implementation by extending AbstractLoader
class imported from @getezy/grpc-client
.
import { AbstractLoader } from '@getezy/grpc-client';
class CustomLoader extends AbstractLoader {
public async load(): Promise<void> {
// custom loader implementation
}
}
Protocol
- is the strategy defines how to make queries to gRPC server.
Uses @grpc/grpc-js.
new GrpcProtocol(options: GrpcProtocolOptions)
import { GrpcProtocol, GrpcTlsType } from '@getezy/grpc-client';
const protocol = new GrpcProtocol({
address: '10.10.10.10',
tls: { type: GrpcTlsType.INSECURE },
channelOptions: { sslTargetNameOverride: 'test' },
});