diff --git a/packages/grpc-js-core/src/client.ts b/packages/grpc-js-core/src/client.ts index 5f9c670b..74de1d3f 100644 --- a/packages/grpc-js-core/src/client.ts +++ b/packages/grpc-js-core/src/client.ts @@ -8,20 +8,24 @@ import {ChannelCredentials} from './channel-credentials'; import {Status} from './constants'; import {Metadata} from './metadata'; +// This symbol must be exported (for now). +// See: https://github.com/Microsoft/TypeScript/issues/20080 +export const kChannel = Symbol(); + export interface UnaryCallback { (err: ServiceError|null, value?: ResponseType): void; } export class Client { - private readonly channel: Channel; + private readonly [kChannel]: Channel; constructor( address: string, credentials: ChannelCredentials, options: Partial = {}) { - this.channel = new Http2Channel(address, credentials, options); + this[kChannel] = new Http2Channel(address, credentials, options); } close(): void { - this.channel.close(); + this[kChannel].close(); } waitForReady(deadline: Date|number, callback: (error: Error|null) => void): @@ -29,7 +33,7 @@ export class Client { let cb: (error: Error|null) => void = once(callback); let callbackCalled = false; let timer: NodeJS.Timer | null = null; - this.channel.connect().then(() => { + this[kChannel].connect().then(() => { if (timer) { clearTimeout(timer); } @@ -140,7 +144,7 @@ export class Client { this.checkOptionalUnaryResponseArguments( metadata, options, callback)); const call: CallStream = - this.channel.createStream(method, metadata, options); + this[kChannel].createStream(method, metadata, options); const message: Buffer = serialize(argument); const writeObj: WriteObject = {message: message}; writeObj.flags = options.flags; @@ -178,7 +182,7 @@ export class Client { this.checkOptionalUnaryResponseArguments( metadata, options, callback)); const call: CallStream = - this.channel.createStream(method, metadata, options); + this[kChannel].createStream(method, metadata, options); this.handleUnaryResponse(call, deserialize, callback); return new ClientWritableStreamImpl(call, serialize); } @@ -222,7 +226,7 @@ export class Client { options?: CallOptions): ClientReadableStream { ({metadata, options} = this.checkMetadataAndOptions(metadata, options)); const call: CallStream = - this.channel.createStream(method, metadata, options); + this[kChannel].createStream(method, metadata, options); const message: Buffer = serialize(argument); const writeObj: WriteObject = {message: message}; writeObj.flags = options.flags; @@ -246,7 +250,7 @@ export class Client { options?: CallOptions): ClientDuplexStream { ({metadata, options} = this.checkMetadataAndOptions(metadata, options)); const call: CallStream = - this.channel.createStream(method, metadata, options); + this[kChannel].createStream(method, metadata, options); return new ClientDuplexStreamImpl( call, serialize, deserialize); } diff --git a/packages/grpc-js-core/src/index.ts b/packages/grpc-js-core/src/index.ts index be03f1bb..b26c390b 100644 --- a/packages/grpc-js-core/src/index.ts +++ b/packages/grpc-js-core/src/index.ts @@ -1,5 +1,40 @@ -export * from './call-credentials'; -export * from './channel-credentials'; -export * from './client'; -export * from './constants'; -export * from './metadata'; + +import { CallCredentials } from './call-credentials'; +import { ChannelCredentials } from './channel-credentials'; +import { Client } from './client'; +import { Status} from './constants'; +import { makeClientConstructor, loadPackageDefinition } from './make-client'; +import { Metadata } from './metadata'; + +const notImplementedFn = () => { throw new Error('Not implemented'); }; + +// Metadata +export { Metadata }; + +// Client credentials + +export const credentials = { + createSsl: ChannelCredentials.createSsl, + createFromMetadataGenerator: CallCredentials.createFromMetadataGenerator, + createFromGoogleCredential: notImplementedFn /*TODO*/, + combineChannelCredentials: (first: ChannelCredentials, ...additional: CallCredentials[]) => additional.reduce((acc, other) => acc.compose(other), first), + combineCallCredentials: (first: CallCredentials, ...additional: CallCredentials[]) => additional.reduce((acc, other) => acc.compose(other), first), + createInsecure: ChannelCredentials.createInsecure +}; + +// Constants + +export { + Status as status + // TODO: Other constants as well +}; + +// Client + +export { + Client, + loadPackageDefinition, + makeClientConstructor, + makeClientConstructor as makeGenericClientConstructor +}; +export const closeClient = (client: Client) => client.close(); diff --git a/packages/grpc-js-core/src/make-client.ts b/packages/grpc-js-core/src/make-client.ts new file mode 100644 index 00000000..ea176a5f --- /dev/null +++ b/packages/grpc-js-core/src/make-client.ts @@ -0,0 +1,149 @@ +import { Metadata } from "./metadata"; +import { Client, UnaryCallback } from "./client"; +import { CallOptions } from "./call-stream"; +import * as _ from 'lodash'; +import { ChannelCredentials } from "./channel-credentials"; +import { ChannelOptions } from "./channel"; + +export interface Serialize { + (value: T): Buffer; +} + +export interface Deserialize { + (bytes: Buffer): T; +} + +export interface MethodDefinition { + path: string; + requestStream: boolean; + responseStream: boolean; + requestSerialize: Serialize; + responseSerialize: Serialize; + requestDeserialize: Deserialize; + responseDeserialize: Deserialize; + originalName?: string; +} + +export interface ServiceDefinition { + [index: string]: MethodDefinition; +} + +export interface PackageDefinition { + [index: string]: ServiceDefinition; +} + +function getDefaultValues(metadata?: Metadata, options?: T): { + metadata: Metadata; + options: Partial; +} { + return { + metadata: metadata || new Metadata(), + options: options || {} + }; +} + +/** + * Map with short names for each of the requester maker functions. Used in + * makeClientConstructor + * @private + */ +const requesterFuncs = { + unary: Client.prototype.makeUnaryRequest, + server_stream: Client.prototype.makeServerStreamRequest, + client_stream: Client.prototype.makeClientStreamRequest, + bidi: Client.prototype.makeBidiStreamRequest +}; + +export interface ServiceClient extends Client { + [methodName: string]: Function; +} + +export interface ServiceClientConstructor { + new(address: string, credentials: ChannelCredentials, + options?: Partial): ServiceClient; + service: ServiceDefinition; +}; + +/** + * Creates a constructor for a client with the given methods, as specified in + * the methods argument. The resulting class will have an instance method for + * each method in the service, which is a partial application of one of the + * [Client]{@link grpc.Client} request methods, depending on `requestSerialize` + * and `responseSerialize`, with the `method`, `serialize`, and `deserialize` + * arguments predefined. + * @param methods An object mapping method names to + * method attributes + * @param serviceName The fully qualified name of the service + * @param classOptions An options object. + * @return New client constructor, which is a subclass of + * {@link grpc.Client}, and has the same arguments as that constructor. + */ +export function makeClientConstructor( + methods: ServiceDefinition, serviceName: string, + classOptions?: {}): ServiceClientConstructor { + if (!classOptions) { + classOptions = {}; + } + + class ServiceClientImpl extends Client implements ServiceClient { + static service: ServiceDefinition; + [methodName: string]: Function; + } + + _.each(methods, (attrs, name) => { + let methodType: keyof typeof requesterFuncs; + // TODO(murgatroid99): Verify that we don't need this anymore + if (_.startsWith(name, '$')) { + throw new Error('Method names cannot start with $'); + } + if (attrs.requestStream) { + if (attrs.responseStream) { + methodType = 'bidi'; + } else { + methodType = 'client_stream'; + } + } else { + if (attrs.responseStream) { + methodType = 'server_stream'; + } else { + methodType = 'unary'; + } + } + const serialize = attrs.requestSerialize; + const deserialize = attrs.responseDeserialize; + const methodFunc = _.partial(requesterFuncs[methodType], attrs.path, + serialize, deserialize); + ServiceClientImpl.prototype[name] = methodFunc; + // Associate all provided attributes with the method + _.assign(ServiceClientImpl.prototype[name], attrs); + if (attrs.originalName) { + ServiceClientImpl.prototype[attrs.originalName] = ServiceClientImpl.prototype[name]; + } + }); + + ServiceClientImpl.service = methods; + + return ServiceClientImpl; +}; + +export type GrpcObject = { + [index: string]: GrpcObject | ServiceClientConstructor; +}; + +export function loadPackageDefinition(packageDef: PackageDefinition) { + const result: GrpcObject = {}; + for (const serviceFqn in packageDef) { + const service = packageDef[serviceFqn]; + const nameComponents = serviceFqn.split('.'); + const serviceName = nameComponents[nameComponents.length-1]; + let current = result; + for (const packageName in nameComponents.slice(0, -1)) { + if (!current[packageName]) { + current[packageName] = {}; + } + current = current[packageName] as GrpcObject; + } + current[serviceName] = makeClientConstructor(service, serviceName, {}); + } + return result; +}