Skip to content

Latest commit

 

History

History
169 lines (118 loc) · 6.09 KB

server_impl_signature.md

File metadata and controls

169 lines (118 loc) · 6.09 KB

@grpc/grpc-js server implementation signature issue

Status

Issue: Issue#79.

This one is fixed in version v5.1.0, and there should be some document to explain more.

Issue

If you are using this tool of version v5.0.1, you may have already noticed a signature issue here: examples/src/grpcjs/server.ts#L74-L75. Note the // @ts-ignore:

// @ts-ignore
server.addService(BookServiceService, new ServerImpl());

The signature of implementation class looks like:

export const BookServiceService: IBookServiceService;

export interface IBookServiceServer {
  getBook: grpc.handleUnaryCall<book_pb.GetBookRequest, book_pb.Book>;
  getBooksViaAuthor: grpc.handleServerStreamingCall<book_pb.GetBookViaAuthor, book_pb.Book>;
  getGreatestBook: handleClientStreamingCall<book_pb.GetBookRequest, book_pb.Book>;
  getBooks: grpc.handleBidiStreamingCall<book_pb.GetBookRequest, book_pb.Book>;
}

class ServerImpl implements IBookServiceServer {
}

The signature of addService is:

export class Server {
  addService(
    service: ServiceDefinition,
    implementation: UntypedServiceImplementation
  ): void {}
}

As you can see the type of implementation need to be UntypedServiceImplementation. So it's inconsistent.

Fixing

The way of fixing is to define IBookServiceServer to extends UntypedServiceImplementation.

export interface IBookServiceServer extends grpc.UntypedServiceImplementation {
}

Though this fixing solved the issue of signature issue here (the original one):

// @ts-ignore
server.addService(BookServiceService, new ServerImpl());

It brings the new issue.

Because UntypedServiceImplementation is defined like:

export interface UntypedServiceImplementation {
  [name: string]: UntypedHandleCall;
}

This means all the attributes inside the UntypedServiceImplementation (and any derived implementation, like class ServerImpl implements IBookServiceServer extends grpc.UntypedServiceImplementation) have to be type of UntypedHandleCall.

So when you define a class to implement IBookServiceServer, you have to add a line of code, like:

class Impl implements IBookServiceServer {
    [name: string]: grpc.UntypedHandleCall;
}

Otherwise, tsc would throw errors: Typescript: Index signature is missing in type.

Implementation

According to this breaking change, there could be two styles of server side implementation. One is Object style, the other is Class style.

Object Style

const Impl: IBookServiceServer = {
    getBook: (call: grpc.ServerUnaryCall<GetBookRequest, Book>, callback: sendUnaryData<Book>): void => {},

    getBooks: (call: grpc.ServerDuplexStream<GetBookRequest, Book>): void => {},

    getBooksViaAuthor: (call: grpc.ServerWritableStream<GetBookViaAuthor, Book>): void => {},

    getGreatestBook: (call: grpc.ServerReadableStream<GetBookRequest, Book>, callback: sendUnaryData<Book>): void => {},
};

const server = new grpc.Server();
server.addService(BookServiceService, Impl);

Class Style

class Impl implements IBookServiceServer {
    [name: string]: grpc.UntypedHandleCall;

    public getBook(call: grpc.ServerUnaryCall<GetBookRequest, Book>, callback: sendUnaryData<Book>): void {}

    public getBooks(call: grpc.ServerDuplexStream<GetBookRequest, Book>) {}

    public getBooksViaAuthor(call: grpc.ServerWritableStream<GetBookViaAuthor, Book>) {}

    public getGreatestBook(call: grpc.ServerReadableStream<GetBookRequest, Book>, callback: sendUnaryData<Book>) {}
}

const server = new grpc.Server();
server.addService(BookServiceService, new Impl());

Only those already defined in the IBookServiceServer can be existing in this Impl class, and [name: string]: grpc.UntypedHandleCall; is required for Class style.

Class style with magic generics

The following is how you can break the restriction above, to add attributes to a class style implementation of a server:

// First we need to set up some magic generics inspired by https://github.com/agreatfool/grpc_tools_node_protoc_ts/issues/79#issuecomment-770173789
type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;

type KnownOnly<T extends Record<any, any>> = Pick<T, KnownKeys<T>>;

// Next we declare a new type using the above generics:
type ITypedBookServer = KnownOnly<IBookServiceServer>;

// Now we declare the class
class Impl implements ITypedBookServer  {
    attr: string;

    constructor(attr: string) {
        this.attr = attr;
    }

    public getBook(call: grpc.ServerUnaryCall<GetBookRequest, Book>, callback: sendUnaryData<Book>): void {}

    public getBooks(call: grpc.ServerDuplexStream<GetBookRequest, Book>) {}

    public getBooksViaAuthor(call: grpc.ServerWritableStream<GetBookViaAuthor, Book>) {}

    public getGreatestBook(call: grpc.ServerReadableStream<GetBookRequest, Book>, callback: sendUnaryData<Book>) {}
}

// now we need to initialize the above Impl. First we need to extend grpc.Server
class TypedServerOverride extends grpc.Server {
    addTypedService<TypedServiceImplementation extends Record<any,any>>(service: ServiceDefinition, implementation: TypedServiceImplementation): void {
        this.addService(service, implementation);
    }
}

// now perform the actual initialization
const server = new TypedServerOverride();
server.addTypedService<ITypedBookServer>(BookServiceService, new Impl("hello world"));

Runnable example could be found here: