nice-grpc

repository·master·Indexed 19 days ago

https://github.com/deeplay-io/nice-grpc

A TypeScript-first gRPC library providing modern, Promise-based and Async Iterable-based APIs for Node.js and Browser environments. The ecosystem includes a core library and various middleware packages for deadlines, devtools logging, automatic retries with exponential backoff, rich error details, and OpenTelemetry instrumentation.

Tokens
38.8K
Snippets
129
Records
170
Agent score
65%

What's inside nice-grpc

  1. Overview of nice-grpc features

    master

    nice-grpc is a TypeScript-first gRPC library designed for modern development workflows. Key features include:

    • TypeScript Native: Written in TypeScript for a seamless developer experience.
    • Modern Async APIs: Uses Promises for unary calls and Async Iterables for streaming data.
    • Cancellation Support: Uses the standard AbortSignal API for easy cancellation propagation.
    • Middleware Support: Provides a concise API for both client and server middleware using Async Generators.
  2. Overview of nice-grpc

    master

    nice-grpc is a Node.js gRPC library built on top of @grpc/grpc-js. It provides a modern, TypeScript-first developer experience with several key features:

    • Modern API: Uses Promises for unary calls and Async Iterables for streaming.
    • Cancellation: Easy cancellation propagation using the standard AbortSignal API.
    • Middleware: Supports both client and server middleware using a concise API based on Async Generators.
    • TypeScript Support: Written in TypeScript to provide excellent type safety and developer ergonomics.
  3. Overview of nice-grpc-web features

    master

    nice-grpc-web is a TypeScript-first gRPC client library designed for the browser. Key features include:

    • Modern API: Uses Promises for unary calls and Async Iterables for streaming.
    • Cancellation: Supports easy cancellation propagation using the standard Web AbortSignal API.
    • Middleware: Provides a concise middleware API implemented via Async Generators.
  4. How the terminator middleware works

    master

    By default, when server.shutdown() is called in nice-grpc, the server stops accepting new calls but waits for all in-flight requests to finish. Long-running calls can block this shutdown indefinitely.

    nice-grpc-server-middleware-terminator solves this by allowing the server to abort active calls during shutdown. When the middleware is terminated, the context.signal of active calls is aborted, and clients receive the gRPC error UNAVAILABLE: Server shutting down.

    Note: The service implementation is still responsible for monitoring the context.signal and aborting its own work.

  5. How idempotency affects retries

    master

    To prevent unsafe side effects, retries are disabled by default unless a method is identified as safe to retry. You can mark methods as safe in your .proto files using idempotency_level options. This requires compiling with ts-proto.

    Supported idempotency levels:

    • IDEMPOTENT: The operation can be safely repeated.
    • NO_SIDE_EFFECTS: The operation is read-only and inherently idempotent.
    service ExampleService {
      rpc ExampleMethod(ExampleMethodRequest) returns (ExampleMethodResponse) {
        option idempotency_level = IDEMPOTENT;
      }
    }
    
    service ExampleService {
      rpc ExampleMethod(ExampleMethodRequest) returns (ExampleMethodResponse) {
        option idempotency_level = NO_SIDE_EFFECTS;
      }
    }
  6. Implement Server Middleware

    master

    Server middleware intercepts calls to execute logic before/after methods, inspect/modify metadata, or augment the CallContext. Middleware is defined as an AsyncGenerator.

    Basic Middleware Structure:

    import {ServerMiddlewareCall, CallContext} from 'nice-grpc';
    
    async function* middleware<Request, Response>(call: ServerMiddlewareCall<Request, Response>, context: CallContext) {
      return yield* call.next(call.request, context);
    }

    Attaching Middleware:

    • Globally: createServer().use(middleware)
    • Per-service: server.with(middleware).add(Service, implementation)

    Note: Middleware attached first is invoked first.

    import {ServerMiddlewareCall, CallContext} from 'nice-grpc';
    
    async function* middleware<Request, Response>(call: ServerMiddlewareCall<Request, Response>, context: CallContext) {
      if (!call.responseStream) {
        const response = yield* call.next(call.request, context);
        return response;
      } else {
        for await (const response of call.next(call.request, context)) {
          yield response;
        }
        return;
      }
    }
    
    const server = createServer().use(middleware);
  7. How client middleware works

    master

    Client middleware intercepts outgoing calls, allowing you to execute logic before/after the call, modify metadata, inspect requests/responses, or implement retries.

    Middleware is defined as an Async Generator.

    Basic Middleware Structure

    For unary and client streaming methods, call.next yields no items and returns a single response. For server streaming and bidirectional streaming, it yields each response.

    import {ClientMiddlewareCall, CallOptions} from 'nice-grpc-web';
    
    async function* middleware<Request, Response>(
      call: ClientMiddlewareCall<Request, Response>,
      options: CallOptions,
    ) {
      if (!call.responseStream) {
        // Unary or Client Streaming
        const response = yield* call.next(call.request, options);
        return response;
      } else {
        // Server Streaming or Bidirectional
        for await (const response of call.next(call.request, options)) {
          yield response;
        }
        return;
      }
    }

    Using a Client Factory

    To attach middleware, use createClientFactory. Middleware attached first is invoked last (it wraps the subsequent ones).

    import {createClientFactory} from 'nice-grpc-web';
    
    const clientFactory = createClientFactory().use(middleware1).use(middleware2);
    
    // Create multiple clients from one factory
    const client1 = clientFactory.create(Service1, channel1);
    const client2 = clientFactory.create(Service2, channel2);
    import {ClientMiddlewareCall, CallOptions} from 'nice-grpc-web';
    
    async function* middleware<Request, Response>(
      call: ClientMiddlewareCall<Request, Response>,
      options: CallOptions,
    ) {
      if (!call.responseStream) {
        const response = yield* call.next(call.request, options);
        return response;
      } else {
        for await (const response of call.next(call.request, options)) {
          yield response;
        }
        return;
      }
    }
  8. Use nice-grpc-common for middleware development

    master

    If you are developing a middleware library for nice-grpc or nice-grpc-web, you should depend on nice-grpc-common instead of the main packages. This provides better semantic versioning stability and allows your middleware to be isomorphic (compatible with both Node.js and Browser environments).

    Note that for standard application code, you should continue to use nice-grpc or nice-grpc-web directly, as they re-export the necessary types from nice-grpc-common.

  9. Implement Server Streaming and Client Streaming

    master

    Server Streaming

    Define the method as an AsyncGenerator that yields responses.

    async *exampleStreamingMethod(request: ExampleRequest, context: CallContext): AsyncIterable<DeepPartial<ExampleResponse>> {
      for (let i = 0; i < 10; i++) {
        yield response;
      }
    }

    Client Streaming

    The method receives the request as an AsyncIterable.

    async exampleClientStreamingMethod(request: AsyncIterable<ExampleRequest>): Promise<DeepPartial<ExampleResponse>> {
      for await (const item of request) {
        // process items
      }
      return response;
    }
  10. Implement Client Middleware

    master

    Client middleware intercepts outgoing calls. Unlike server middleware, the invocation order is reversed (the first middleware attached is the last one invoked). Client middleware uses CallOptions instead of CallContext.

    Creating a Client with Middleware: Use createClientFactory() to chain middleware before creating the client.

    import {createClientFactory} from 'nice-grpc';
    
    const client = createClientFactory()
      .use(middleware1)
      .use(middleware2)
      .create(ExampleService, channel);

    Type Augmentation: If your middleware adds properties to CallOptions, you can annotate the client type:

    let client: ExampleServiceClient<{myCustomOption?: number}>;
    client = createClientFactory().use(middleware).create(ExampleService, channel);
    import {ClientMiddlewareCall, CallOptions} from 'nice-grpc';
    
    async function* middleware<Request, Response>(call: ClientMiddlewareCall<Request, Response>, options: CallOptions) {
      return yield* call.next(call.request, options);
    }
    
    const clientFactory = createClientFactory().use(middleware);
    const client = clientFactory.create(ExampleService, channel);
  11. Customize gRPC metrics with custom instances

    master

    You can provide your own metric instances to the middleware. This is useful if you want to use custom histogram buckets or a custom registry. When using custom metrics, ensure you use the correct labelNames (either labelNames or labelNamesWithCode) to avoid errors.

    import {createClientFactory} from 'nice-grpc';
    import {
      labelNamesWithCode,
      prometheusClientMiddleware,
    } from 'nice-grpc-prometheus';
    import {Histogram, Registry} from 'prom-client';
    
    const registry = new Registry();
    
    const clientHandlingSecondsMetric = new Histogram({
      registers: [registry],
      name: 'custom_grpc_client_handling_seconds',
      help: 'Custom histogram of response latency (seconds) of the gRPC until it is finished by the application.',
      labelNames: labelNamesWithCode,
      buckets: [0.1, 0.5, 1, 2, 3, 5, 10],
    });
    
    const clientFactory = createClientFactory()
      .use(prometheusClientMiddleware({clientHandlingSecondsMetric}))
      .use(/* ... other middleware */);
    
    // Remember to merge this registry with your global registry