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(),
})),
};
}
}
@smnbbrv
Copy link
Author

smnbbrv commented Jan 28, 2022

This allows to use promisified version of the @grpc-js client with all types preserved. This method is the least invasive, since it preserves the original signatures for client and its calls, only the callback is removed and still leaves the original non-promisified methods under $ property.

Of course, the promisification only works for unary calls.

@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