Skip to content

Instantly share code, notes, and snippets.

@smnbbrv
Last active November 4, 2023 21:22
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save smnbbrv/f147fceb4c29be5ce877b6275018e294 to your computer and use it in GitHub Desktop.
Save smnbbrv/f147fceb4c29be5ce877b6275018e294 to your computer and use it in GitHub Desktop.
Promisify @grpc-js service client with typescript
import { Client, ServiceError, Metadata, CallOptions, ClientUnaryCall } from '@grpc/grpc-js';
import { Message } from 'google-protobuf';
type OriginalCall<T, U> = (request: T, metadata: Metadata, options: Partial<CallOptions>, callback: (error: ServiceError, res: U) => void) => ClientUnaryCall;
type PromisifiedCall<T, U> = ((request: T, metadata?: Metadata, options?: Partial<CallOptions>) => Promise<U>);
export type Promisified<C> = { $: C; } & {
[prop in Exclude<keyof C, keyof Client>]: (C[prop] extends OriginalCall<infer T, infer U> ? PromisifiedCall<T, U> : never);
}
export function promisify<C extends Client>(client: C): Promisified<C> {
return new Proxy(client, {
get: (target, descriptor) => {
let stack = '';
// this step is required to get the correct stack trace
// of course, this has some performance impact, but it's not that big in comparison with grpc calls
try { throw new Error(); } catch (e) { stack = e.stack; }
if (descriptor === '$') {
return target;
}
return (...args: any[]) => new Promise((resolve, reject) => target[descriptor](...[...args, (err: ServiceError, res: Message) => {
if (err) {
err.stack += stack;
reject(err);
} else {
resolve(res);
}
}]));
},
}) as unknown as Promisified<C>;
}
export class SearchService {
private searchServiceClient: Promisified<SearchServiceClient>;
constructor(private config: Config) {
const { host, grpcPort } = this.config.services.shopCore;
this.searchServiceClient = promisify(new SearchServiceClient(`${host}:${grpcPort}`, ChannelCredentials.createInsecure()));
}
async search(query: string, limit: number): Promise<SearchResult> {
const request = new SearchRequest().setQuery(query);
const response = await this.searchServiceClient.search(request);
return {
items: response.getResultsList().map(item => ({
name: item.getName(),
url: item.getUrl(),
})),
};
}
}
@awx-erik-yu
Copy link

I got a Element implicitly has an 'any' type because expression of type 'string | symbol' can't be used to index type 'Client'. No index signature with a parameter of type 'string' was found on type 'Client'. error on line 19, target[descriptor].

I don't know if I had a wrong typescript setting.

image

@smnbbrv
Copy link
Author

smnbbrv commented May 25, 2022

You should explicitly use generate_package_definition if you use https://www.npmjs.com/package/grpc_tools_node_protoc_ts . Example: grpc_tools_node_protoc --plugin=protoc-gen-ts=../../node_modules/.bin/protoc-gen-ts --ts_out=generate_package_definition:... ...

@josefschabasser
Copy link

@WillAbides
Copy link

This was a big help.

I needed to make a small change to Promisified<C> to make it work on clients that have both unary and streaming calls. I changed the : never at the end to : any.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment