npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@omkarkirpan/grpc-client

v1.0.4

Published

A gRPC client library that is nice to you

Downloads

64

Readme

@omkarkirpan/grpc-client npm version

A Node.js gRPC library that is nice to you. Built on top of grpc-js.

Features

  • Written in TypeScript for TypeScript.
  • Modern API that uses Promises and Async Iterables for streaming.
  • Cancelling client and server calls using AbortSignal.
  • Client and server middleware support via concise API that uses Async Generators.

Installation

npm install @omkarkirpan/grpc-client google-protobuf @grpc/grpc-js
npm install --save-dev @types/google-protobuf

Usage

Compiling Protobuf files

This works the same way as you would do for grpc-js.

Install necessary tools:

npm install --save-dev grpc-tools grpc_tools_node_protoc_ts

Given a Protobuf file ./proto/example.proto, generate JS code and TypeScript definitions into directory ./compiled_proto:

./node_modules/.bin/grpc_tools_node_protoc \
  --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
  --plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin \
  --js_out=import_style=commonjs,binary:./compiled_proto \
  --ts_out=grpc_js:./compiled_proto \
  --grpc_out=grpc_js:./compiled_proto \
  ./proto/example.proto

for windows, if you are facing issue genrating compiled proto files check this workaround https://stackoverflow.com/questions/59447763/node-js-grpc-out-protoc-gen-grpc-1-is-not-a-valid-win32-application

Alternative methods include Buf and Prototool.

Client

Consider the following Protobuf definition:

syntax = "proto3";

package pkg.example;

service ExampleService {
  rpc ExampleUnaryMethod(ExampleRequest) returns (ExampleResponse) {};
}

message ExampleRequest {
  // ...
}
message ExampleResponse {
  // ...
}

After compiling Protobuf file, we can create the client:

import {createChannel, createClient} from '@omkarkirpan/grpc-client';
import {ExampleService} from './compiled_proto/example_grpc_pb';

const channel = createChannel('localhost:8080');

const client = createClient(ExampleService, channel);

When creating a client, you can specify default call options for all methods, or per-method. See Example: Timeouts.

Call the method:

import {ExampleRequest, ExampleResponse} from './compiled_proto/example_pb';

const response: ExampleResponse = await client.exampleUnaryMethod(
  new ExampleRequest(),
);

Once we've done with the client, close the channel:

client.close();

Channels

By default, a channel uses insecure connection. The following are equivalent:

import {ChannelCredentials} from '@grpc/grpc-js';
import {createChannel} from '@omkarkirpan/grpc-client';

createChannel('example.com:8080');
createChannel('http://example.com:8080');
createChannel('example.com:8080', ChannelCredentials.createInsecure());

To connect over TLS, use one of the following:

createChannel('https://example.com:8080');
createChannel('example.com:8080', ChannelCredentials.createSsl());

Metadata

Client can send request metadata and receive response headers and trailers:

import {Metadata} from '@grpc/grpc-js';

const metadata = new Metadata();
metadata.set('key', 'value');

const response = await client.exampleUnaryMethod(new ExampleRequest(), {
  metadata,
  onHeader(header: Metadata) {
    // ...
  },
  onTrailer(trailer: Metadata) {
    // ...
  },
});

Errors

Client calls may throw gRPC errors represented as ClientError, that contain status code and description.

import {status} from '@grpc/grpc-js';
import {ClientError} from '@omkarkirpan/grpc-client';

let response: ExampleResponse | null;

try {
  response = await client.exampleUnaryMethod(new ExampleRequest());
} catch (error: unknown) {
  if (error instanceof ClientError && error.code === status.NOT_FOUND) {
    response = null;
  } else {
    throw error;
  }
}

Cancelling calls

A client call can be cancelled using AbortSignal.

import AbortController from 'node-abort-controller';
import {isAbortError} from '@omkarkirpan/abort-controller-x';

const abortController = new AbortController();

client
  .exampleUnaryMethod(new ExampleRequest(), {
    signal: abortController.signal,
  })
  .catch(error => {
    if (isAbortError(error)) {
      // aborted
    } else {
      throw error;
    }
  });

abortController.abort();

Deadlines

You can specify a deadline for a client call using Date object:

import {status} from '@grpc/grpc-js';
import {ClientError} from '@omkarkirpan/grpc-client';
import {addSeconds} from 'date-fns';

try {
  const response = await client.exampleUnaryMethod(new ExampleRequest(), {
    deadline: addSeconds(new Date(), 15),
  });
} catch (error: unknown) {
  if (error instanceof ClientError && error.code === status.DEADLINE_EXCEEDED) {
    // timed out
  } else {
    throw error;
  }
}

Server streaming

Consider the following Protobuf definition:

service ExampleService {
  rpc ExampleStreamingMethod(ExampleRequest)
    returns (stream ExampleResponse) {};
}

Client method returns an Async Iterable:

for await (const response of client.exampleStreamingMethod(
  new ExampleRequest(),
)) {
  // ...
}

Client streaming

Given a client streaming method:

service ExampleService {
  rpc ExampleClientStreamingMethod(stream ExampleRequest)
    returns (ExampleResponse) {};
}

Client method expects an Async Iterable as its first argument:

async function* createRequest(): AsyncIterable<ExampleRequest> {
  for (let i = 0; i < 10; i++) {
    yield new ExampleRequest();
  }
}

const response = await client.exampleClientStreamingMethod(createRequest());

Middleware

Client middleware intercepts outgoing calls allowing to:

  • Execute any logic before and after reaching server
  • Modify request metadata
  • Look into request, response and response metadata
  • Send call multiple times for retries or hedging
  • Augment call options type to have own configuration

Client middleware is defined as an Async Generator and is very similar to Server middleware. Key differences:

  • Middleware invocation order is reversed: middleware that is attached first, will be invoked last.
  • There's no such thing as CallContext for client middleware; instead, CallOptions are passed through the chain and can be accessed or altered by a middleware.

To create a client with middleware, use a client factory:

import {createClientFactory} from '@omkarkirpan/grpc-client';

const client = createClientFactory()
  .use(middleware1)
  .use(middleware2)
  .create(ExampleService, channel);

A middleware that is attached first, will be invoked last.

You can reuse a single factory to create multiple clients:

const clientFactory = createClientFactory().use(middleware);

const client1 = clientFactory.create(Service1, channel1);
const client2 = clientFactory.create(Service2, channel2);

You can also attach middleware per-client:

const factory = createClientFactory().use(middlewareA);

const client1 = clientFactory.use(middlewareB).create(Service1, channel1);
const client2 = clientFactory.use(middlewareC).create(Service2, channel2);

In the above example, Service1 client gets middlewareA and middlewareB, and Service2 client gets middlewareA and middlewareC.

Example: Logging

Log all calls:

import {
  ClientMiddlewareCall,
  CallOptions,
  ClientError,
} from '@omkarkirpan/grpc-client';
import {isAbortError} from '@omkarkirpan/abort-controller-x';

async function* loggingMiddleware<Request, Response>(
  call: ClientMiddlewareCall<Request, Response>,
  options: CallOptions,
) {
  const {path} = call.definition;

  console.log('Client call', path, 'start');

  try {
    const result = yield* call.next(call.request, options);

    console.log('Client call', path, 'end: OK');

    return result;
  } catch (error) {
    if (error instanceof ClientError) {
      console.log('Client call', path, `end: ${status[error.code]}`);
    } else if (isAbortError(error)) {
      console.log('Client call', path, 'cancel');
    } else {
      console.log('Client call', path, `error: ${error?.stack}`);
    }

    throw error;
  }
}
Example: Timeouts

Add support for specifying timeouts for unary calls instead of absolute deadlines:

import ms = require('ms');
import {ClientMiddlewareCall, CallOptions} from '@omkarkirpan/grpc-client';

type TimeoutCallOptionsExt = {
  /**
   * Examples: '10s', '1m'
   */
  timeout?: string;
};

async function* timeoutMiddleware<Request, Response>(
  call: ClientMiddlewareCall<Request, Response>,
  options: CallOptions & TimeoutCallOptionsExt,
) {
  const {timeout, ...nextOptions} = options;

  if (timeout != null && !call.requestStream && !call.responseStream) {
    nextOptions.deadline ??= new Date(Date.now() + ms(timeout));
  }

  return yield* call.next(call.request, nextOptions);
}

When creating a client, you can specify default call options for all methods, or per-method:

const client = createClientFactory()
  .use(timeoutMiddleware)
  .create(ExampleService, channel, {
    '*': {
      timeout: '1m',
    },
    exampleUnaryMethod: {
      timeout: '30s',
    },
  });

Specify call options per-call:

await client.exampleUnaryMethod(new ExampleRequest(), {
  timeout: '15s',
});