Skip to content

Commit

Permalink
feat: server build-in grpc reflection api
Browse files Browse the repository at this point in the history
  • Loading branch information
chakhsu committed Jan 9, 2024
1 parent 883d16a commit ee9fe71
Show file tree
Hide file tree
Showing 15 changed files with 366 additions and 16 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Here is the feature:
- **No-Route**: No routing, RPC is inherently bound to methods.
- **Middleware**: Both client and server support middleware.
- **Metadata**: Standardizes the transmission and retrieval of metadata.
- **Reflection**: Built-in gRPC Reflection API in server.
- **Error**: Provides dedicated Error objects to ensure targeted handling of
exceptions after catching.
- **Promise**: Supports promisify internally in RPC methods while also
Expand Down
1 change: 1 addition & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- **No-Route**: 无路由,rpc 与 method 天生绑定;
- **Middleware**: 客户端和服务端都支持中间件机制;
- **Metadata**: 规范化了元信息的传递和获取;
- **Reflection**: 服务端内置 gRPC reflection API;
- **Error**: 提供了专有 Error 对象,保证异常捕捉后可以针对性处理;
- **Promise**: rpc 方法内部支持了 promisify,同时也保留了 callbackify ;
- **Config**: 与官方配置对齐,支持 pb load 配置和 gRPC channel 配置;
Expand Down
77 changes: 77 additions & 0 deletions example/reflection/asyncStreamServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { asyncStreamLoader as loader } from './loader.js'

function timeout(ms) {
return new Promise((resolve, reject) => setTimeout(resolve, ms))
}

class Stream {
async unaryHello(call) {
return { message: 'hello ' + call.request.message }
}

async clientStreamHello(call) {
const metadata = call.metadata.clone()
metadata.add('x-timestamp-server', 'received=' + new Date().toISOString())
call.sendMetadata(metadata)

for await (const data of call.readAll()) {
console.log(data)
}
return { message: "Hello! I'm fine, thank you!" }
}

async serverStreamHello(call) {
const metadata = call.metadata.clone()
metadata.add('x-timestamp-server', 'received=' + new Date().toISOString())
call.sendMetadata(metadata)

console.log(call.request.message)
call.write({ message: 'Hello! I got you message.' })
call.write({ message: "I'm fine, thank you" })
call.writeAll([{ message: 'other thing x' }, { message: 'other thing y' }])
call.end()
}

async mutualStreamHello(call) {
const metadata = call.metadata.clone()
metadata.add('x-timestamp-server', 'received=' + new Date().toISOString())
call.sendMetadata(metadata)

call.write({ message: 'emmm...' })

for await (const data of call.readAll()) {
console.log(data.message)
if (data.message === 'Hello!') {
call.write({ message: 'Hello too.' })
} else if (data.message === 'How are you?') {
call.write({ message: "I'm fine, thank you" })
await timeout(1000)
call.write({ message: 'delay 1s' })
call.writeAll([{ message: 'emm... ' }, { message: 'emm......' }])
} else {
call.write({ message: 'pardon?' })
}
}

call.end()
}
}

const start = async (addr) => {
await loader.init({
isDev: true,
packagePrefix: 'test'
})
const reflection = await loader.initReflection()

const server = await loader.initServer()

server.add('stream.Hellor', new Stream())

server.inject(reflection)

await server.listen(addr)
console.log('start:', addr)
}

start('localhost:9097')
107 changes: 107 additions & 0 deletions example/reflection/helloworldServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { helloworldLoader as loader } from './loader.js'

const timeout = (ms) => {
return new Promise((resolve, reject) => setTimeout(resolve, ms))
}

class Greeter {
init(server) {
server.add('helloworld.Greeter', this, { exclude: ['init'] })
}

async sayGreet(call) {
const metadata = call.metadata.clone()
metadata.add('x-timestamp-server', 'received=' + new Date().toISOString())
call.sendMetadata(metadata)
if (metadata.get('x-throw-error').length > 0) {
throw new Error('throw error because x-throw-error')
}

if (metadata.get('x-long-delay').length > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000 * 10))
}
await timeout(1000)

return {
message: `hello, ${call.request.name || 'world'}`
}
}

async sayGreet2(call) {
return this.sayGreet(call)
}
}

class Hellor {
constructor() {
this.count = 0
}

init(server) {
server.add('helloworld.Hellor', this, { exclude: ['init'] })
}

async SayHello(call) {
const metadata = call.metadata.clone()
metadata.add('x-timestamp-server', 'received=' + new Date().toISOString())
call.sendMetadata(metadata)
if (metadata.get('x-throw-error').length > 0) {
throw new Error('throw error because x-throw-error')
}

if (metadata.get('x-long-delay').length > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000 * 10))
}

this.count++

return {
message: `hello, ${call.request.name || 'world'}`,
count: this.count
}
}

async SayHello2(call) {
return this.SayHello(call)
}
}

const middlewareA = async (ctx, next) => {
const beginTime = new Date().getTime()
console.log('middlewareA: 1', ctx, beginTime)
await timeout(1000)
await next()
await timeout(1000)
const endTime = new Date().getTime()
console.log('middlewareA: 2', ctx, endTime, endTime - beginTime)
}

const middlewareB = async (ctx, next) => {
const beginTime = new Date().getTime()
console.log('middlewareB: 1', ctx, beginTime)
await next()
const endTime = new Date().getTime()
console.log('middlewareB: 2', ctx, endTime, endTime - beginTime)
}

const start = async (addr) => {
await loader.init({
isDev: true,
packagePrefix: 'test'
})

const reflection = await loader.initReflection()

const server = await loader.initServer()
server.use(middlewareA, middlewareB)

const servicers = [new Greeter(), new Hellor()]
servicers.map((s) => s.init(server))

server.inject(reflection)

await server.listen(addr)
console.log('start:', addr)
}

start('localhost:9098')
17 changes: 17 additions & 0 deletions example/reflection/loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ProtoLoader } from '../../lib/index.js'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

// get this file dir path
const __dirname = path.dirname(fileURLToPath(import.meta.url))

export const helloworldLoader = new ProtoLoader({
location: path.join(__dirname, '../proto'),
files: ['helloworld/service.proto']
})

export const asyncStreamLoader = new ProtoLoader({
location: path.join(__dirname, '../proto'),
files: ['stream/service.proto']
})
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "grpcity",
"version": "2.0.0",
"version": "2.1.0",
"description": "A powerful and complete gRPC framework for Node.js",
"author": "Chakhsu.Lau",
"license": "MIT",
Expand Down Expand Up @@ -28,6 +28,7 @@
},
"files": [
"lib",
"docs",
"README_CN.md"
],
"scripts": {
Expand All @@ -44,7 +45,9 @@
"dependencies": {
"@grpc/grpc-js": "^1.9.13",
"@grpc/proto-loader": "^0.7.10",
"joi": "^17.11.0"
"@grpc/reflection": "^1.0.1",
"joi": "^17.11.0",
"protobufjs": "^7.2.5"
},
"devDependencies": {
"@tsconfig/node14": "^14.1.0",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type { HandleUnaryCall, ServerUnaryCall } from './server/unaryCallProxy'
export type { HandleClientStreamingCall, ServerReadableStream } from './server/clientStreamingCallProxy'
export type { HandleServerStreamingCall, ServerWritableStream } from './server/serverStreamingCallProxy'
export type { HandleBidiStreamingCall, ServerDuplexStream } from './server/bidiStreamingCallProxy'
export type { ReflectionServerOptions } from './server/serverReflection'

// export grpc-js
export type { Metadata, StatusObject } from '@grpc/grpc-js'
Expand Down
15 changes: 12 additions & 3 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import * as grpc from '@grpc/grpc-js'
import * as protoLoader from '@grpc/proto-loader'

import Server from './server'
import { Reflection, ReflectionServerOptions } from './server/serverReflection'
import Clients from './client'
import { isString } from './utils/string'
import { get } from './utils/object'
import { prefixingDefinition } from './utils/definition'
import { assertProtoFileOptionsOptions, attemptInitOptions, attemptInitClientsOptions } from './schema/loader'
import { assertProtoFileOptionsOptions, attemptInitOptions, attemptInitClientsOptions, attemptInitServerOptions } from './schema/loader'
import type { ClientsOptions, ServerOptions } from './schema/loader'
import type { ProtoFileOptions, ProtoFileOptionType, InitOptions } from './schema/loader'

Expand Down Expand Up @@ -48,7 +49,7 @@ export class ProtoLoader {

const packageDefinition = await protoLoader.load(files, loadOptions)

if (this._packagePrefix) {
if (this._isDev && this._packagePrefix) {
this._packageDefinition = prefixingDefinition(packageDefinition, packagePrefix)
} else {
this._packageDefinition = packageDefinition
Expand All @@ -72,7 +73,15 @@ export class ProtoLoader {
if (!this._packageDefinition) {
await this.init()
}
return new Server(this, options)
const serverOptions = attemptInitServerOptions(options)
return new Server(this, serverOptions)
}

async initReflection(options?: ReflectionServerOptions) {
if (!this._packageDefinition) {
await this.init()
}
return new Reflection(this._packageDefinition as protoLoader.PackageDefinition, options)
}

makeClientCredentials(rootCerts?: Buffer, privateKey?: Buffer, certChain?: Buffer, verifyOptions?: any) {
Expand Down
17 changes: 15 additions & 2 deletions src/schema/loader.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import Joi from 'joi'
import type { Options as LoaderOptions } from '@grpc/proto-loader'
import type { ChannelOptions, ChannelCredentials } from '@grpc/grpc-js'
import type { ChannelOptions, ChannelCredentials, ServerCredentials } from '@grpc/grpc-js'
import { defaultLoadOptions } from '../config/defaultLoadOptions'
import { defaultChannelOptions } from '../config/defaultChannelOptions'

export type { Options as LoaderOptions } from '@grpc/proto-loader'
export type { ChannelOptions as ServerOptions } from '@grpc/grpc-js'

const protoFileOptionsSchema = Joi.array()
.items(
Expand Down Expand Up @@ -74,3 +73,17 @@ export type ClientsOptions = {
export const attemptInitClientsOptions = (options: ClientsOptions) => {
return Joi.attempt(options || {}, ClientsOptionsSchema)
}

const ServerOptionsSchema = Joi.object({
channelOptions: Joi.object().optional().default(defaultChannelOptions),
credentials: Joi.any().optional()
})

export type ServerOptions = {
channelOptions?: ChannelOptions
credentials?: ServerCredentials
}

export const attemptInitServerOptions = (options?: ServerOptions): ServerOptions => {
return Joi.attempt(options || {}, ServerOptionsSchema)
}
3 changes: 1 addition & 2 deletions src/schema/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import Joi from 'joi'

import type { ChannelOptions } from '@grpc/grpc-js'
import { defaultChannelOptions } from '../config/defaultChannelOptions'
import { ServerOptions } from './loader'

export const assignServerOptions = (options?: ServerOptions): ChannelOptions => {
export const assignServerChannelOptions = (options?: ChannelOptions): ChannelOptions => {
return Object.assign({}, defaultChannelOptions, options || {})
}
Loading

0 comments on commit ee9fe71

Please sign in to comment.