diff --git a/src/experimental/index.ts b/src/experimental/index.ts new file mode 100644 index 0000000..9c05bf6 --- /dev/null +++ b/src/experimental/index.ts @@ -0,0 +1,154 @@ +import { AnyDefinition } from '@grpc/proto-loader'; +import * as grpc from 'grpc'; +import { ServiceDefinition } from 'grpc'; +import { Observable, Subscribable } from 'rxjs'; + +import { + ClientFactoryConstructor, + createServiceClient, + DynamicMethods, + getServiceNames, + grpcLoad, + GrpcService, + lookupPackage, + protoLoad, +} from '../utils'; + +type LooseReturnType = T extends (...args: any[]) => infer R ? R : never; +type RequestType = T extends (arg: infer A, ...args: any[]) => any ? A : never; +type ResponseType = LooseReturnType extends Subscribable ? A : never; + +export type SerializedMessage = Buffer & { + __deserialized_type?: T; +}; + +type Serializer = (message: T) => SerializedMessage; + +type Deserializer = (serialized: SerializedMessage) => T; + +type MethodCodecs = { + requestSerialize: Serializer>; + requestDeserialize: Deserializer>; + responseSerialize: Serializer>; + responseDeserialize: Deserializer>; +}; + +type ServiceCodecs = { [MethodName in keyof Service]: MethodCodecs }; + +type CodecsFactory = { + [GetterName in keyof ClientFactory]: () => ServiceCodecs< + LooseReturnType + > +}; + +type RawServiceMethod = ( + request: SerializedMessage>, + metadata?: grpc.Metadata, +) => Observable>>; + +type RawService = { [MethodName in keyof Service]: RawServiceMethod }; + +type RawClientFactory = { + [GetterName in keyof ClientFactory]: () => RawService> +}; + +export function rawClientFactory(protoPath: string, packageName: string) { + class Constructor { + readonly __args: [string, grpc.ChannelCredentials, any | undefined]; + constructor(address: string, credentials?: grpc.ChannelCredentials, options: any = undefined) { + this.__args = [address, credentials || grpc.credentials.createInsecure(), options]; + } + } + + const prototype: DynamicMethods = Constructor.prototype; + const pkg = lookupPackage(grpcLoad(protoPath), packageName); + for (const name of getServiceNames(pkg)) { + type GetterType = keyof ClientFactory; + type ServiceType = LooseReturnType; + prototype[`get${name}`] = function(this: Constructor) { + const grpcService = pkg[name] as GrpcService; + const definition = grpcService.service; + typedKeys(definition).forEach(methodKey => { + const methodDefinition = definition[methodKey]; + methodDefinition.requestSerialize = (alreadySerialized: SerializedMessage) => { + return alreadySerialized; + }; + methodDefinition.responseDeserialize = (serialized: SerializedMessage) => { + return serialized; + }; + }); + return createServiceClient(grpcService, this.__args); + }; + } + + return (Constructor as any) as ClientFactoryConstructor>; +} + +export function buildCodecsFactory( + protoPath: string, + packageName: string, +): CodecsFactory { + const factory = {} as CodecsFactory; + type GetterType = keyof ClientFactory; + type ServiceType = LooseReturnType; + const services = loadServices(protoPath, packageName); + for (const name of typedKeys(services)) { + const codecs = buildServiceCodecs(services[name]); + const getterName = `get${name}` as GetterType; + factory[getterName] = () => codecs; + } + return factory; +} + +function buildServiceCodecs( + definition: ServiceDefinition, +): ServiceCodecs { + const codecs = {} as { [key in keyof Service]: MethodCodecs }; + typedKeys(definition).forEach(methodKey => { + getMethodNames(definition, methodKey).forEach(methodName => { + const { + requestSerialize, + requestDeserialize, + responseSerialize, + responseDeserialize, + } = definition[methodKey]; + codecs[methodName] = { + requestSerialize, + requestDeserialize, + responseSerialize, + responseDeserialize, + }; + }); + }); + return codecs; +} + +function getMethodNames(definition: ServiceDefinition, key: keyof T) { + const methodDefinition = definition[key]; + return [key, (methodDefinition as any).originalName as keyof T]; +} + +function loadServices(protoPath: string, packageName: string) { + const packageDefinition = protoLoad(protoPath); + const prefix = packageName + '.'; + const pkg: Record> = {}; + Object.keys(packageDefinition) + .filter(key => key.startsWith(prefix)) + .forEach(key => { + const entry = packageDefinition[key]; + if (isServiceDefinition(entry)) { + pkg[key.substring(prefix.length)] = entry; + } + }); + return pkg; +} + +function isServiceDefinition( + def: AnyDefinition | ServiceDefinition, +): def is ServiceDefinition { + return typeof def['format'] !== 'string'; +} + +function typedKeys(t: T) { + return Object.keys(t) as Array>; +} diff --git a/src/utils.ts b/src/utils.ts index 5166e1d..da85de2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -30,13 +30,16 @@ export function lookupPackage(root: GrpcObject, packageName: string) { return pkg; } -export function grpcLoad(protoPath: string) { - const packageDefinition = protoLoader.loadSync(protoPath, { +export function protoLoad(protoPath: string) { + return protoLoader.loadSync(protoPath, { keepCase: true, defaults: true, oneofs: true, }); - return loadPackageDefinition(packageDefinition); +} + +export function grpcLoad(protoPath: string) { + return loadPackageDefinition(protoLoad(protoPath)); } export function createService(Service: GrpcService, rxImpl: DynamicMethods) {