Skip to content

Instantly share code, notes, and snippets.

@smnbbrv
Last active September 27, 2024 21:53
Show Gist options
  • 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(),
})),
};
}
}
@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.

@bhalash
Copy link

bhalash commented Sep 27, 2024

Lovely bit of code, thank for for this. :) I modified it to take non-unary calls, for metadata:

type Callback<T> = (err: ServiceError | null, reply: T) => void;

type Call<T, A1> = (message: A1, callback: Callback<T>) => UnaryResponse;
type Promisified<T, A1> = (message: A1) => Promise<T>;
type CallWithMetadata<T, A1> = (message: A1, metadata: Metadata, callback: Callback<T>) => UnaryResponse;
type PromisifiedWithMetadata<T, A1> = (message: A1, metadata: Metadata) => Promise<T>;

export type PromisifiedClient<T> = { $: T } & {
  [K in keyof T]
    : T[K] extends Call<infer T, infer A1> ? Promisified<T, A1>
    : T[K] extends CallWithMetadata<infer T, infer A1> ? PromisifiedWithMetadata<T, A1>
    : never;
}

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