Last active
September 27, 2024 21:53
-
-
Save smnbbrv/f147fceb4c29be5ce877b6275018e294 to your computer and use it in GitHub Desktop.
Promisify @grpc-js service client with typescript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | |
})), | |
}; | |
} | |
} |
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:... ...
Fix strict mode (and better naming): timostamm/protobuf-ts#345 (comment)
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
.
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
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.