@tgrospic/rnode-grpc-js
Version:
RNode gRPC helpers
291 lines (250 loc) • 9.22 kB
text/typescript
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)
}