UNPKG

@tgrospic/rnode-grpc-js

Version:
291 lines (250 loc) 9.22 kB
import * as R from 'ramda' import { protoTsTypesMapping, flattenSchema } from './lib' const { log, warn } = console const isBrowser = typeof window === 'object' && !!window.document // Global protobuf object generated by grpc-tool !!! // @ts-ignore const PROTO = () => proto const getProtoSerializer = R.curry((getType, name) => { const {constructor} = getType(name) const serialize = (obj: any) => { const t = fillObject(getType, name, obj) return t.serializeBinary() /* Uint8Array */ } const deserialize = (bytes: any) => { const msg = constructor.deserializeBinary(bytes) return msg.toObject() } const create = (opt: any) => { return new constructor(opt) } return { serialize, deserialize, create } }) type ProtoSchema = { protoSchema: any } type MakeGrpcProtocolArg = { host: string grpcLib: any clientOptions: any } & ProtoSchema const makeGrpcProtocol = ({host, grpcLib, clientOptions={}}: MakeGrpcProtocolArg) => { if (!grpcLib) throw Error(`gRPC library not provided (grpcLib option).`) // gRPC clients if (isBrowser) { if (!grpcLib.AbstractClientBase) throw Error(`Browser detected but 'grpc-web' instance not recognized.`) // Browser support (grpc-web) const MethodInfo = grpcLib.AbstractClientBase.MethodInfo // https://github.com/grpc/grpc-web/blob/8b501a96f/javascript/net/grpc/web/grpcwebclientbase.js#L45 const client = new grpcLib.GrpcWebClientBase({format: 'binary', ...clientOptions}) return { client, MethodInfo } } else { if (!grpcLib.Client) throw Error(`Node.js detected but 'grpc' instance not recognized.`) // Nodejs support (grpc-js) const { credentials, ...options } = clientOptions const creds = credentials || grpcLib.credentials.createInsecure() // https://github.com/grpc/grpc-node/blob/b05caec/packages/grpc-js/src/client.ts#L67 const client = new grpcLib.Client(host, creds, options) return { client } } } const init = ({protoSchema}: ProtoSchema) => { const schemaFlat = R.chain(flattenSchema([]), [protoSchema]) const isType = ({type}: any) => type === 'type' const isName = (typeName: string) => ({name}: any) => name === typeName const types = R.filter(isType, schemaFlat) // Returns type definition and type constructor (protoc generated) const getType = (name: string) => { const typeDef = R.find(isName(name), types) const { namespace }: any = typeDef const typePath = R.lensPath([...namespace, name]) // Get type constructor from `proto` global object generated by `protoc` tool return { constructor: R.view(typePath, PROTO()), def: typeDef, } } // Methods defined in protobufjs generated JSON schema const methods = R.pipe( R.filter(R.propEq('type', 'service')), R.map((service: any) => R.map(m => ({...m, service}), service.methods)), R.mergeAll, )(schemaFlat) return { getType, methods, types } } const resolveEither = (eitherObj: any) => { const { success, error } = eitherObj if (success) { const {value: valBytes, typeUrl} = success.response const [_, typeFullName] = typeUrl.match(/^type.rchain.coop\/(.+)$/) const typeLens = R.lensPath(typeFullName.split('.')) // Get type constructor from `proto` global object generated by `protoc` tool const typeDef: any = R.view(typeLens, PROTO()) // Deserialize message and convert to JS object return typeDef.deserializeBinary(valBytes).toObject() } else if (error) { const { messagesList } = error throw Error(`Either error: ${messagesList.join(', ')}`) } } const resolveError = R.curry((typeName, getType, msgObj) => { if (typeName === 'Either') { return resolveEither(msgObj) } // Detect errors inside message const respTypeDef = getType(typeName) const errorTypeLens = R.lensPath('def.fields.error.type'.split('.')) const errorType = R.view(errorTypeLens, respTypeDef) // Throw message errors (ServiceError) if (errorType === 'ServiceError' && msgObj.error) { throw Error(`Service error: ${msgObj.error.messagesList.join(', ')}`) } return msgObj }) const simpleTypes = R.map(R.prop('proto'), protoTsTypesMapping) const fillObject = R.curry((getType, typeName, input) => { const type = getType(typeName) if (!type) throw Error(`Type not found: ${typeName}`) const req = new type.constructor() Object.entries(input || {}).forEach(([key, v]) => { const field = type.def.fields[key] if (!field) { warn(`Property not found: ${typeName}.${key}`) return } // Handle collections (proto repeated) const isListType = field.rule === 'repeated' const [fst, snd, ...tail] = key const setterName = (f: Function) => R.flatten( ['set', fst.toUpperCase(), snd, f(tail.join(''))] ).join('') const setter = req[setterName(R.identity)] || req[setterName(R.toLower)] !setter && warn( `Property setter not found ${typeName}.${key} (<gen-js>.${setterName(R.identity)})` ) // Create property value / recursively resolve complex types const val = R.includes(field.type, simpleTypes) // Simple type ? v // Complex type : isListType ? R.map(fillObject(getType, field.type), v) : fillObject(getType, field.type, v) // Set property value setter.bind(req)(val) }) return req }) const createApiMethod = R.curry(({protocol, host}, getType, method, name) => async (input: any, meta: any) => { const isReponseStream = !!method.responseStream // Build service method name const namespace = method.service.namespace.join('.') const serviceName = method.service.name const methodName = `/${namespace}.${serviceName}/${name}` // Request/response protobuf serializers const reqProto = getProtoSerializer(getType, method.requestType) const resProto = getProtoSerializer(getType, method.responseType) const client = protocol.client if (isBrowser) { // Browser support (grpc-web) const methodInfo = new protocol.MethodInfo(null, reqProto.serialize, resProto.deserialize) // Select type of method const remoteMethod = isReponseStream ? client.serverStreaming.bind(client) : client.unaryCall.bind(client) // Call remote method const comm = remoteMethod( `${host}${methodName}`, input, meta || {}, methodInfo, ) if (isReponseStream) { return new Promise((resolve, reject) => { const streamResult: any[] = [] comm.on('data', (resultMsg: any) => { try { const result = resolveError(method.responseType, getType, resultMsg) streamResult.push(result) } catch (err) { reject(err) } }) comm.on('error', reject) comm.on('end', (_: any) => { resolve(streamResult) }) }) } else { return comm.then(resolveError(method.responseType, getType)) } } else { // Nodejs support (grpc-js) if (isReponseStream) { // Call remote method const comm = client.makeServerStreamRequest( methodName, R.pipe(reqProto.serialize, Buffer.from), resProto.deserialize, input, meta || {}, ) return new Promise((resolve, reject) => { const streamResult: any = [] comm.on('data', (resultMsg: any) => { try { const result = resolveError(method.responseType, getType, resultMsg) streamResult.push(result) } catch (err) { reject(err) } }) comm.on('error', reject) comm.on('end', (_: any) => { resolve(streamResult) }) }) } else { return new Promise((resolve, reject) => { // Call remote method client.makeUnaryRequest( methodName, R.pipe(reqProto.serialize, Buffer.from), resProto.deserialize, input, meta || {}, (err: any, resultMsg: string) => { if (err) reject(err) else { try { // Resolve Either value const result = resolveError(method.responseType, getType, resultMsg) resolve(result) } catch (err) { reject(err) } } } ) }) } } }) /** @internal */ export const rnodeService = (options: MakeGrpcProtocolArg) => { const {getType, methods} = init(options) // Create client protocol const protocol = makeGrpcProtocol(options) // Create RNode service API from proto definition const service = R.mapObjIndexed( // @ts-ignore createApiMethod({ ...options, protocol }, getType), methods, ) return { ...service, _grpcClient: protocol.client } } // Different name for each service to support TypeScript definitions /** @internal */ export const rnodeDeploy = rnodeService /** @internal */ export const rnodePropose = rnodeService /** @internal */ export const rnodeRepl = rnodeService /** @internal */ export const rnodeProtobuf = ({protoSchema}: ProtoSchema) => { const {getType, types} = init({protoSchema}) const getTypeOp = ({name}: any) => ({ [name]: getProtoSerializer(getType, name) }) return R.pipe(R.map(getTypeOp), R.mergeAll)(types) }