├── src ├── server │ ├── index.ts │ ├── service_info.ts │ ├── store.ts │ ├── store.spec.ts │ ├── service.ts │ ├── call_store.ts │ ├── grpc.ts │ ├── server.ts │ ├── call.ts │ └── server.spec.ts ├── proto │ ├── index.ts │ ├── interfaces.ts │ └── definitions.ts ├── index.ts ├── client │ ├── index.ts │ ├── call.ts │ ├── client.ts │ ├── service.ts │ └── client.spec.ts ├── mock │ ├── index.ts │ ├── builder.ts │ ├── interfaces.ts │ ├── server.ts │ └── definitions.ts └── index.spec.ts ├── .gitignore ├── .npmignore ├── jasmine.json ├── .travis.yml ├── scripts ├── build_touchups.bash ├── build_types.js ├── gen_proto.bash └── gen_typings.js ├── test └── run_tests.js ├── tsconfig.json ├── proto ├── mock.proto └── grpc-bus.proto ├── LICENSE ├── package.json ├── tslint.json └── README.md /src/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server'; 2 | -------------------------------------------------------------------------------- /src/proto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './definitions'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | dist/ 4 | coverage/ 5 | npm-debug.log 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server'; 2 | export * from './client'; 3 | export * from './proto'; 4 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export { ICallHandle } from './call'; 3 | export { IServiceHandle } from './service'; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | e2e/ 2 | proto/mock.proto 3 | src/ 4 | scripts/ 5 | coverage/ 6 | jasmine.json 7 | tslint.json 8 | tsconfig.json 9 | test/ 10 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './definitions'; 3 | export * from './builder'; 4 | export * from './server'; 5 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "./src", 3 | "spec_files": [ "**/*.spec.ts" ], 4 | "stopSpecOnExpectationFailure": true, 5 | "random": false 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | 5 | script: 6 | - npm run ci 7 | 8 | after_success: 9 | - bash <(curl -s https://codecov.io/bash) 10 | -------------------------------------------------------------------------------- /scripts/build_touchups.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Touching up reference to es6-promise..." 4 | echo " -> https://github.com/apollostack/apollo-client/issues/861" 5 | sed -i -e '/es6-promise/d' ./lib/**/*.d.ts ./lib/*.d.ts 6 | -------------------------------------------------------------------------------- /src/mock/builder.ts: -------------------------------------------------------------------------------- 1 | import { PROTO_DEFINITIONS } from './definitions'; 2 | 3 | let ProtoBuf = require('protobufjs'); 4 | export function buildTree(): any { 5 | return ProtoBuf.loadJson(JSON.stringify(PROTO_DEFINITIONS)); 6 | } 7 | -------------------------------------------------------------------------------- /src/mock/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IHelloRequest { 2 | name?: string; 3 | } 4 | 5 | export interface IHelloReply { 6 | message?: string; 7 | } 8 | 9 | export const enum EDummyEnum { 10 | DUMMY = 0, 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/server/service_info.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IGBServiceInfo, 3 | } from '../proto'; 4 | 5 | // Generates a unique string identifier for service connection info. 6 | export function buildServiceInfoIdentifier(info: IGBServiceInfo): string { 7 | // Return endpoint for now. 8 | return info.endpoint; 9 | } 10 | -------------------------------------------------------------------------------- /test/run_tests.js: -------------------------------------------------------------------------------- 1 | var Jasmine = require('jasmine'); 2 | var jasmine = new Jasmine(); 3 | var JasmineConsoleReporter = require('jasmine-console-reporter'); 4 | 5 | var reporter = new JasmineConsoleReporter({ 6 | colors: 1, // (0|false)|(1|true)|2 7 | cleanStack: 1, // (0|false)|(1|true)|2|3 8 | verbosity: 4, // (0|false)|1|2|(3|true)|4 9 | listStyle: 'indent', // "flat"|"indent" 10 | activity: false 11 | }); 12 | 13 | jasmine.loadConfigFile('jasmine.json'); 14 | jasmine.addReporter(reporter); 15 | jasmine.execute(); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": false, 4 | "preserveConstEnums": true, 5 | "sourceMap": true, 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "suppressImplicitAnyIndexErrors": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "target": "ES5", 12 | "outDir": "lib/" 13 | }, 14 | "formatCodeOptions": { 15 | "indentSize": 2, 16 | "tabSize": 2 17 | }, 18 | "files": [ 19 | "src/index.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /proto/mock.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mock; 3 | 4 | // The greeting service definition. 5 | service Greeter { 6 | // Sends a greeting 7 | rpc SayHello (HelloRequest) returns (HelloReply) {} 8 | rpc SayHelloClientStream(stream HelloRequest) returns (HelloReply) {} 9 | rpc SayHelloServerStream(HelloRequest) returns (stream HelloReply) {} 10 | rpc SayHelloBidiStream(stream HelloRequest) returns (stream HelloReply) {} 11 | } 12 | 13 | enum EDummyEnum { 14 | DUMMY = 0; 15 | } 16 | 17 | // The request message containing the user's name. 18 | message HelloRequest { 19 | string name = 1; 20 | } 21 | 22 | // The response message containing the greetings 23 | message HelloReply { 24 | string message = 1; 25 | } 26 | -------------------------------------------------------------------------------- /src/server/store.ts: -------------------------------------------------------------------------------- 1 | import { buildServiceInfoIdentifier } from './service_info'; 2 | import { 3 | IGBServiceInfo, 4 | } from '../proto'; 5 | import { Service } from './service'; 6 | 7 | // A store of active services. 8 | export class ServiceStore { 9 | private services: { [id: string]: Service } = {}; 10 | 11 | public constructor(private protoTree: any, private grpc: any) {} 12 | 13 | // Get service for a client. 14 | public getService(clientId: number, info: IGBServiceInfo): Service { 15 | let identifier = buildServiceInfoIdentifier(info); 16 | let serv: Service = this.services[identifier]; 17 | if (!serv) { 18 | serv = new Service(this.protoTree, clientId, info, this.grpc); 19 | serv.disposed.subscribe(() => { 20 | if (this.services) { 21 | delete this.services[identifier]; 22 | } 23 | }); 24 | this.services[identifier] = serv; 25 | } else { 26 | serv.clientAdd(clientId); 27 | } 28 | return serv; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/server/store.spec.ts: -------------------------------------------------------------------------------- 1 | import { ServiceStore } from './store'; 2 | import { IGBServiceInfo } from '../proto'; 3 | import { 4 | buildTree, 5 | } from '../mock'; 6 | 7 | describe('ServiceStore', () => { 8 | let store: ServiceStore; 9 | let info: IGBServiceInfo = { 10 | endpoint: 'localhost:3000', 11 | service_id: 'mock.Greeter', 12 | }; 13 | 14 | beforeEach(() => { 15 | store = new ServiceStore(buildTree(), require('grpc')); 16 | }); 17 | 18 | it('should get a service correctly', () => { 19 | let serv = store.getService(1, info); 20 | expect(serv).not.toBe(null); 21 | }); 22 | 23 | it('should dispose a service correctly', () => { 24 | let spai = jasmine.createSpy('Dispose Func'); 25 | let serv = store.getService(10, info); 26 | let servb = store.getService(11, info); 27 | serv.initStub(); 28 | expect(servb).toBe(serv); 29 | serv.disposed.subscribe(spai); 30 | serv.clientRelease(10); 31 | expect(spai).not.toHaveBeenCalled(); 32 | serv.clientRelease(11); 33 | expect(spai).toHaveBeenCalled(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Christian Stewart = new Subject(); 8 | 9 | public sayHello(request: IHelloRequest, callback: any) { 10 | this.callReceived.next('sayHello'); 11 | callback(null, {message: 'Hello'}); 12 | } 13 | 14 | public sayHelloClientStream(call: any, callback: any) { 15 | call.on('data', (data: any) => { 16 | this.callReceived.next('sayHelloClientStream'); 17 | }); 18 | call.on('end', () => { 19 | callback(null, {message: 'Hello'}); 20 | }); 21 | } 22 | 23 | public sayHelloServerStream(call: any) { 24 | this.callReceived.next('sayHelloServerStream'); 25 | call.write({ 26 | message: 'Hello', 27 | }); 28 | call.end(); 29 | } 30 | 31 | public sayHelloBidiStream(call: any) { 32 | this.callReceived.next('sayHelloBidiStream'); 33 | call.on('data', (data: any) => { 34 | call.write({message: data.name}); 35 | }); 36 | call.on('end', () => { 37 | call.end(); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/build_types.js: -------------------------------------------------------------------------------- 1 | function processEnum(types, enm) { 2 | if (enm.name && enm.values && enm.values.length > 0) { 3 | var typ = types[enm.name] = {isEnum: true}; 4 | for (var i = 0; i < enm.values.length; i++) { 5 | var valo = enm.values[i]; 6 | typ[valo.name] = valo.id; 7 | } 8 | } 9 | } 10 | 11 | function processMessage(types, msg) { 12 | if (msg.name && msg.fields && msg.fields.length > 0) { 13 | var typ = types[msg.name] = {}; 14 | for (var i = 0; i < msg.fields.length; i++) { 15 | var f = msg.fields[i]; 16 | typ[f.name] = f; 17 | } 18 | } 19 | 20 | if (msg.messages != null) { 21 | for (var i = 0; i < msg.messages.length; i++) { 22 | processMessage(types, msg.messages[i]); 23 | } 24 | } 25 | 26 | if (msg.enums != null) { 27 | for (var i = 0; i < msg.enums.length; i++) { 28 | processEnum(types, msg.enums[i]); 29 | } 30 | } 31 | } 32 | 33 | if (!module.parent) { 34 | (function() { 35 | var data = ""; 36 | var types = {}; 37 | process.stdin.resume(); 38 | process.stdin.on('data', function(buf) { data += buf.toString(); }); 39 | process.stdin.on('end', function() { 40 | processMessage(types, JSON.parse(data)); 41 | console.log(types); 42 | }); 43 | })(); 44 | } 45 | 46 | module.exports = { 47 | processEnum: processEnum, 48 | processMessage: processMessage, 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grpc-bus", 3 | "homepage": "https://github.com/paralin/grpc-bus#readme", 4 | "description": "Call GRPC services (even streams!) from the browser.", 5 | "author": "Christian Stewart ", 6 | "license": "MIT", 7 | "main": "./lib/index.js", 8 | "types": "./lib/index.d.ts", 9 | "bugs": { 10 | "url": "https://github.com/paralin/grpc-bus/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/paralin/grpc-bus.git" 15 | }, 16 | "scripts": { 17 | "build": "tsc && ./scripts/build_touchups.bash", 18 | "gen-proto": "./scripts/gen_proto.bash", 19 | "lint": "tslint --project tsconfig.json -c tslint.json --type-check", 20 | "test": "npm run lint && npm run mocha", 21 | "ci": "npm run build && npm run test", 22 | "mocha": "ts-node ./test/run_tests.js cover -e .ts -x \"*.d.ts\" -x \"*.spec.ts\" node_modules/jasmine/bin/jasmine.js", 23 | "mocha-nocover": "ts-node ./test/run_tests.js" 24 | }, 25 | "dependencies": { 26 | "lodash": "^4.0.0", 27 | "rxjs": "5.0.0-rc.5" 28 | }, 29 | "devDependencies": { 30 | "@types/es6-promise": "0.0.32", 31 | "@types/jasmine": "^2.5.36", 32 | "@types/lodash": "^4.14.34", 33 | "@types/node": "^6.0.51", 34 | "babel-core": "^6.18.0", 35 | "babel-preset-es2015": "^6.18.0", 36 | "cz-conventional-changelog": "^1.2.0", 37 | "grpc": "^1.0.0", 38 | "istanbul": "^1.1.0-alpha.1", 39 | "jasmine": "^2.5.0", 40 | "jasmine-console-reporter": "^1.2.0", 41 | "protobufjs": "^5.0.0", 42 | "ts-node": "^1.7.0", 43 | "tslint": "^4.0.0", 44 | "typescript": "^2.1.0" 45 | }, 46 | "config": { 47 | "commitizen": { 48 | "path": "./node_modules/cz-conventional-changelog" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/gen_proto.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | JSON_OUTPUT_PATH=./.tmp.json 3 | PBJS=./node_modules/protobufjs/bin/pbjs 4 | 5 | if [ ! -d "./scripts" ]; then 6 | if [ -n "$ATTEMPTED_CD_DOTDOT" ]; then 7 | echo "You need to run this from the root of the project." 8 | exit 1 9 | fi 10 | set -e 11 | cd ../ && ATTEMPTED_CD_DOTDOT=yes $@ 12 | exit 0 13 | fi 14 | 15 | # Check if we need to update pbjs 16 | # https://github.com/dcodeIO/protobuf.js/pull/470 17 | if ! grep -q "\"request_stream\": mtd.requestStream," ./node_modules/protobufjs/cli/pbjs/targets/json.js ; then 18 | echo "Updating protobuf.js to tip..." 19 | echo " -> see https://github.com/dcodeIO/protobuf.js/pull/470" 20 | npm install github:dcodeIO/protobuf.js\#f2661c32e 21 | fi 22 | 23 | write_definitions() { 24 | JSON_PATH=$1 25 | DEFS_PATH=$2 26 | INTERFACES_PATH=$3 27 | 28 | echo "Generated json, $(cat $JSON_PATH | wc -l) lines." 29 | echo "/* tslint:disable:trailing-comma */" > $DEFS_PATH 30 | echo "/* tslint:disable:quotemark */" >> $DEFS_PATH 31 | echo "/* tslint:disable:max-line-length */" >> $DEFS_PATH 32 | echo "export const PROTO_DEFINITIONS = $(cat ${JSON_PATH});" >> $DEFS_PATH 33 | 34 | cat $JSON_PATH | node ./scripts/gen_typings.js > $INTERFACES_PATH 35 | } 36 | 37 | set -e 38 | ${PBJS} \ 39 | -p ${GOPATH}/src \ 40 | -t json \ 41 | ./proto/grpc-bus.proto > \ 42 | ${JSON_OUTPUT_PATH} 43 | sed -i -e "s/proto2/proto3/g" $JSON_OUTPUT_PATH 44 | 45 | write_definitions ${JSON_OUTPUT_PATH} \ 46 | ./src/proto/definitions.ts \ 47 | ./src/proto/interfaces.ts 48 | rm ${JSON_OUTPUT_PATH} || true 49 | 50 | # Generate mocks 51 | ${PBJS} \ 52 | -p ${GOPATH}/src \ 53 | -t json \ 54 | ./proto/mock.proto > \ 55 | ${JSON_OUTPUT_PATH} 56 | sed -i -e "s/proto2/proto3/g" $JSON_OUTPUT_PATH 57 | 58 | write_definitions ${JSON_OUTPUT_PATH} \ 59 | ./src/mock/definitions.ts \ 60 | ./src/mock/interfaces.ts 61 | rm ${JSON_OUTPUT_PATH} || true 62 | -------------------------------------------------------------------------------- /scripts/gen_typings.js: -------------------------------------------------------------------------------- 1 | var processMessage = require("./build_types.js").processMessage; 2 | 3 | const typeMap = { 4 | "string": "string", 5 | "int32": "number", 6 | "uint32": "number", 7 | "double": "number", 8 | "float": "number", 9 | // long? XXX 10 | "int64": "number", 11 | "uint64": "number", 12 | "sint32": "number", 13 | "sint64": "number", 14 | "fixed32": "number", 15 | "fixed64": "number", 16 | "sfixed32": "number", 17 | "sfixed64": "number", 18 | "bool": "boolean", 19 | // base64? XXX 20 | "bytes": "any", 21 | }; 22 | 23 | function buildTypings(defs) { 24 | var types = {}; 25 | 26 | processMessage(types, defs); 27 | 28 | // var result = "namespace Proto {\n"; 29 | var result = ""; 30 | for (var typen in types) { 31 | result += "export "; 32 | var type = types[typen]; 33 | if (type.isEnum) { 34 | result += "const enum " + typen + " {\n"; 35 | for (var valuen in type) { 36 | if (valuen === "isEnum") { 37 | continue; 38 | } 39 | var val = type[valuen]; 40 | result += " " + valuen + " = " + val + ",\n"; 41 | } 42 | result += "}\n\n"; 43 | continue; 44 | } 45 | 46 | result += "interface I" + typen + " {\n" 47 | for (var fieldn in type) { 48 | var field = type[fieldn]; 49 | var typ = field.type; 50 | var typp = typ.split("."); 51 | typ = typp[typp.length - 1]; 52 | var typm = typeMap[typ]; 53 | var typit = types[typ]; 54 | if (typm) { 55 | typ = typm; 56 | } else if (typit) { 57 | if (!typit.isEnum) { 58 | typ = "I" + typ; 59 | } 60 | } else { 61 | typ = "any"; 62 | } 63 | if (field.rule === "repeated") { 64 | typ = "" + typ + "[]"; 65 | } else if (field.rule === "map") { 66 | typ = `{ [key: string]: ${typ} }`; 67 | } 68 | result += " " + fieldn + (field.rule === "optional" ? "?" : "") + ": " + typ + ";\n"; 69 | } 70 | result += "}\n\n"; 71 | } 72 | // result += "}"; 73 | console.log(result); 74 | } 75 | 76 | (function() { 77 | var data = ""; 78 | process.stdin.resume(); 79 | process.stdin.on('data', function(buf) { data += buf.toString(); }); 80 | process.stdin.on('end', function() { 81 | buildTypings(JSON.parse(data)); 82 | }); 83 | })(); 84 | -------------------------------------------------------------------------------- /src/server/service.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs/Subject'; 2 | import { 3 | IGBServiceInfo, 4 | } from '../proto'; 5 | import { 6 | loadObject, 7 | } from './grpc'; 8 | 9 | import * as _ from 'lodash'; 10 | 11 | // A stored service. 12 | export class Service { 13 | // Subject called when disposed. 14 | public disposed: Subject = new Subject(); 15 | // GRPC service stub 16 | public stub: any; 17 | // Service metadata 18 | public serviceTree: any; 19 | // Service info 20 | private info: IGBServiceInfo; 21 | // List of client service IDs corresponding to this service. 22 | private clientIds: number[]; 23 | 24 | constructor(private protoTree: any, 25 | clientId: number, 26 | info: IGBServiceInfo, 27 | // Pass require('grpc') as an argument. 28 | private grpc: any) { 29 | this.clientIds = [clientId]; 30 | this.info = info; 31 | } 32 | 33 | public initStub() { 34 | let serv = this.protoTree.lookup(this.info.service_id); 35 | if (!serv) { 36 | throw new TypeError(this.info.service_id + ' was not found.'); 37 | } 38 | if (serv.className !== 'Service') { 39 | throw new TypeError(this.info.service_id + ' is a ' + serv.className + ' not a Service.'); 40 | } 41 | let stubctr = loadObject(this.grpc, serv); 42 | this.stub = new stubctr(this.info.endpoint, this.grpc.credentials.createInsecure()); 43 | this.serviceTree = serv; 44 | } 45 | 46 | public lookupMethod(methodId: string): any { 47 | for (let child of this.serviceTree.children) { 48 | if (child.name === methodId) { 49 | return child; 50 | } 51 | } 52 | return null; 53 | } 54 | 55 | public clientAdd(id: number) { 56 | if (this.clientIds.indexOf(id) === -1) { 57 | this.clientIds.push(id); 58 | } 59 | } 60 | 61 | public clientRelease(id: number) { 62 | if (!this.clientIds) { 63 | return; 64 | } 65 | this.clientIds = _.without(this.clientIds, id); 66 | if (this.clientIds.length === 0) { 67 | this.destroy(); 68 | } 69 | } 70 | 71 | private destroy() { 72 | this.clientIds = null; 73 | this.disposed.next(this); 74 | if (this.stub) { 75 | this.grpc.getClientChannel(this.stub).close(); 76 | } 77 | this.stub = null; 78 | this.info = null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/proto/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IGBClientMessage { 2 | service_create?: IGBCreateService; 3 | service_release?: IGBReleaseService; 4 | call_create?: IGBCreateCall; 5 | call_end?: IGBCallEnd; 6 | call_send?: IGBSendCall; 7 | } 8 | 9 | export interface IGBServerMessage { 10 | service_create?: IGBCreateServiceResult; 11 | service_release?: IGBReleaseServiceResult; 12 | call_create?: IGBCreateCallResult; 13 | call_event?: IGBCallEvent; 14 | call_ended?: IGBCallEnded; 15 | } 16 | 17 | export interface IGBServiceInfo { 18 | endpoint?: string; 19 | service_id?: string; 20 | } 21 | 22 | export interface IGBCreateService { 23 | service_id?: number; 24 | service_info?: IGBServiceInfo; 25 | } 26 | 27 | export interface IGBReleaseService { 28 | service_id?: number; 29 | } 30 | 31 | export interface IGBCallInfo { 32 | method_id?: string; 33 | bin_argument?: any; 34 | } 35 | 36 | export interface IGBCreateCall { 37 | service_id?: number; 38 | call_id?: number; 39 | info?: IGBCallInfo; 40 | } 41 | 42 | export interface IGBCallEnded { 43 | call_id?: number; 44 | service_id?: number; 45 | } 46 | 47 | export interface IGBEndCall { 48 | call_id?: number; 49 | service_id?: number; 50 | } 51 | 52 | export interface IGBSendCall { 53 | call_id?: number; 54 | service_id?: number; 55 | bin_data?: any; 56 | is_end?: boolean; 57 | } 58 | 59 | export interface IGBCreateServiceResult { 60 | service_id?: number; 61 | result?: ECreateServiceResult; 62 | error_details?: string; 63 | } 64 | 65 | export const enum ECreateServiceResult { 66 | SUCCESS = 0, 67 | INVALID_ID = 1, 68 | GRPC_ERROR = 2, 69 | } 70 | 71 | export interface IGBReleaseServiceResult { 72 | service_id?: number; 73 | } 74 | 75 | export interface IGBCreateCallResult { 76 | call_id?: number; 77 | service_id?: number; 78 | result?: ECreateCallResult; 79 | error_details?: string; 80 | } 81 | 82 | export const enum ECreateCallResult { 83 | SUCCESS = 0, 84 | INVALID_ID = 1, 85 | GRPC_ERROR = 2, 86 | } 87 | 88 | export interface IGBCallEvent { 89 | call_id?: number; 90 | service_id?: number; 91 | event?: string; 92 | json_data?: string; 93 | bin_data?: any; 94 | } 95 | 96 | export interface IGBCallEnd { 97 | call_id?: number; 98 | service_id?: number; 99 | } 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/server/call_store.ts: -------------------------------------------------------------------------------- 1 | import { Service } from './service'; 2 | import { Call } from './call'; 3 | import { 4 | IGBServerMessage, 5 | IGBCreateCall, 6 | IGBCreateCallResult, 7 | IGBSendCall, 8 | IGBEndCall, 9 | } from '../proto'; 10 | 11 | // Store of all active calls for a client service. 12 | export class CallStore { 13 | private calls: { [id: number]: Call } = {}; 14 | 15 | public constructor(private service: Service, 16 | private clientId: number, 17 | private send: (msg: IGBServerMessage) => void) { 18 | } 19 | 20 | public initCall(msg: IGBCreateCall) { 21 | let result: IGBCreateCallResult = { 22 | call_id: msg.call_id, 23 | service_id: msg.service_id, 24 | result: 0, 25 | }; 26 | if (typeof msg.call_id !== 'number' || this.calls[msg.call_id]) { 27 | // todo: fix enums 28 | result.result = 1; 29 | result.error_details = 'ID is not set or is already in use.'; 30 | } else { 31 | try { 32 | let callId = msg.call_id; 33 | let call = new Call(this.service, msg.call_id, msg.service_id, msg.info, this.send); 34 | call.initCall(); 35 | this.calls[msg.call_id] = call; 36 | call.disposed.subscribe(() => { 37 | this.releaseLocalCall(callId); 38 | }); 39 | } catch (e) { 40 | result.result = 2; 41 | result.error_details = e.toString(); 42 | } 43 | } 44 | 45 | this.send({ 46 | call_create: result, 47 | }); 48 | } 49 | 50 | public handleCallEnd(msg: IGBEndCall) { 51 | let call = this.calls[msg.call_id]; 52 | if (!call) { 53 | return; 54 | } 55 | call.dispose(); 56 | } 57 | 58 | public handleCallWrite(msg: IGBSendCall) { 59 | let call = this.calls[msg.call_id]; 60 | if (!call) { 61 | return; 62 | } 63 | if (msg.is_end) { 64 | call.sendEnd(); 65 | } else { 66 | call.write(msg.bin_data); 67 | } 68 | } 69 | 70 | public releaseLocalCall(id: number) { 71 | delete this.calls[id]; 72 | } 73 | 74 | // Kill all ongoing calls, cleanup. 75 | public dispose() { 76 | for (let callId in this.calls) { 77 | if (!this.calls.hasOwnProperty(callId)) { 78 | continue; 79 | } 80 | this.calls[callId].dispose(); 81 | } 82 | this.service.clientRelease(this.clientId); 83 | this.calls = {}; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/mock/definitions.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:trailing-comma */ 2 | /* tslint:disable:quotemark */ 3 | /* tslint:disable:max-line-length */ 4 | export const PROTO_DEFINITIONS = { 5 | "package": "mock", 6 | "syntax": "proto3", 7 | "messages": [ 8 | { 9 | "name": "HelloRequest", 10 | "fields": [ 11 | { 12 | "rule": "optional", 13 | "type": "string", 14 | "name": "name", 15 | "id": 1 16 | } 17 | ], 18 | "syntax": "proto3" 19 | }, 20 | { 21 | "name": "HelloReply", 22 | "fields": [ 23 | { 24 | "rule": "optional", 25 | "type": "string", 26 | "name": "message", 27 | "id": 1 28 | } 29 | ], 30 | "syntax": "proto3" 31 | } 32 | ], 33 | "enums": [ 34 | { 35 | "name": "EDummyEnum", 36 | "values": [ 37 | { 38 | "name": "DUMMY", 39 | "id": 0 40 | } 41 | ], 42 | "syntax": "proto3" 43 | } 44 | ], 45 | "services": [ 46 | { 47 | "name": "Greeter", 48 | "options": {}, 49 | "rpc": { 50 | "SayHello": { 51 | "request": "HelloRequest", 52 | "request_stream": false, 53 | "response": "HelloReply", 54 | "response_stream": false, 55 | "options": {} 56 | }, 57 | "SayHelloClientStream": { 58 | "request": "HelloRequest", 59 | "request_stream": true, 60 | "response": "HelloReply", 61 | "response_stream": false, 62 | "options": {} 63 | }, 64 | "SayHelloServerStream": { 65 | "request": "HelloRequest", 66 | "request_stream": false, 67 | "response": "HelloReply", 68 | "response_stream": true, 69 | "options": {} 70 | }, 71 | "SayHelloBidiStream": { 72 | "request": "HelloRequest", 73 | "request_stream": true, 74 | "response": "HelloReply", 75 | "response_stream": true, 76 | "options": {} 77 | } 78 | } 79 | } 80 | ], 81 | "isNamespace": true 82 | }; 83 | -------------------------------------------------------------------------------- /src/server/grpc.ts: -------------------------------------------------------------------------------- 1 | // GRPC methods copied from the GRPC codebase. 2 | // This is to not have a dependence on grpc at runtime in this library. 3 | import * as _ from 'lodash'; 4 | 5 | // Service object passed to makeClientConstructor 6 | // https://github.com/grpc/grpc/issues/8727 7 | export interface IGRPCMethodObject { 8 | path: string; 9 | requestStream: boolean; 10 | responseStream: boolean; 11 | requestType: any; 12 | responseType: any; 13 | requestSerialize: (msg: any) => any; 14 | requestDeserialize: (msg: any) => any; 15 | responseSerialize: (msg: any) => any; 16 | responseDeserialize: (msg: any) => any; 17 | } 18 | 19 | export interface IGRPCServiceObject { 20 | [methodName: string]: IGRPCMethodObject; 21 | } 22 | 23 | export function fullyQualifiedName(meta: any): string { 24 | if (meta === null || meta === undefined) { 25 | return ''; 26 | } 27 | let name = meta.name; 28 | let parentName = fullyQualifiedName(meta.parent); 29 | if (parentName && parentName.length) { 30 | name = parentName + '.' + name; 31 | } 32 | return name; 33 | } 34 | 35 | function ensureBuffer(inp: any): Buffer { 36 | if (typeof inp === 'string') { 37 | return new Buffer(inp); 38 | } 39 | if (typeof inp === 'object') { 40 | // detect ByteBuffer 41 | if (inp.constructor !== Buffer) { 42 | return inp.toBuffer(); 43 | } 44 | } 45 | return inp; 46 | } 47 | 48 | export function getPassthroughServiceAttrs(service: any, options: any): IGRPCServiceObject { 49 | let prefix = '/' + fullyQualifiedName(service) + '/'; 50 | let res: IGRPCServiceObject = {}; 51 | for (let method of service.children) { 52 | res[_.camelCase(method.name)] = { 53 | path: prefix + method.name, 54 | requestStream: method.requestStream, 55 | responseStream: method.responseStream, 56 | requestType: method.requestType, 57 | responseType: method.responseType, 58 | requestSerialize: ensureBuffer, 59 | requestDeserialize: _.identity, 60 | responseSerialize: ensureBuffer, 61 | responseDeserialize: _.identity, 62 | }; 63 | } 64 | return res; 65 | } 66 | 67 | export function makePassthroughClientConstructor(grpc: any, service: any, options: any): any { 68 | let methodAttrs = getPassthroughServiceAttrs(service, options); 69 | let Client = grpc.makeGenericClientConstructor( 70 | methodAttrs, fullyQualifiedName(service), 71 | false, 72 | ); 73 | Client.service = service; 74 | Client.service.grpc_options = options; 75 | return Client; 76 | } 77 | 78 | // Modified loadObject that removes deserialization. 79 | export function loadObject(grpc: any, meta: any, options?: any): any { 80 | let result: any = {}; 81 | if (meta.className === 'Namespace') { 82 | _.each(meta.children, (child: any) => { 83 | result[child.name] = loadObject(grpc, child, options); 84 | }); 85 | return result; 86 | } else if (meta.className === 'Service') { 87 | return makePassthroughClientConstructor(grpc, meta, options); 88 | } else if (meta.className === 'Message' || meta.className === 'Enum') { 89 | return meta.build(); 90 | } else { 91 | return meta; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "member-access": false, 4 | "member-ordering": [ 5 | true, 6 | "public-before-private", 7 | "static-before-instance", 8 | "variables-before-functions" 9 | ], 10 | "no-any": false, 11 | "no-inferrable-types": false, 12 | "no-internal-module": true, 13 | "no-var-requires": false, 14 | "typedef": false, 15 | "typedef-whitespace": [ 16 | true, 17 | { 18 | "call-signature": "nospace", 19 | "index-signature": "nospace", 20 | "parameter": "nospace", 21 | "property-declaration": "nospace", 22 | "variable-declaration": "nospace" 23 | }, 24 | { 25 | "call-signature": "space", 26 | "index-signature": "space", 27 | "parameter": "space", 28 | "property-declaration": "space", 29 | "variable-declaration": "space" 30 | } 31 | ], 32 | 33 | "ban": false, 34 | "curly": false, 35 | "forin": true, 36 | "label-position": true, 37 | "no-arg": true, 38 | "no-bitwise": true, 39 | "no-conditional-assignment": true, 40 | "no-console": true, 41 | "no-construct": true, 42 | "no-debugger": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": true, 45 | "no-eval": true, 46 | "no-null-keyword": false, 47 | "no-shadowed-variable": true, 48 | "no-string-literal": false, 49 | "no-switch-case-fall-through": true, 50 | "no-unused-expression": true, 51 | "no-use-before-declare": true, 52 | "no-var-keyword": true, 53 | "radix": true, 54 | "switch-default": true, 55 | "triple-equals": [ 56 | true, 57 | "allow-null-check" 58 | ], 59 | 60 | "eofline": true, 61 | "indent": [ 62 | true, 63 | "spaces" 64 | ], 65 | "max-line-length": [ 66 | true, 67 | 100 68 | ], 69 | "no-require-imports": false, 70 | "no-trailing-whitespace": true, 71 | "object-literal-sort-keys": false, 72 | "trailing-comma": [ 73 | true, 74 | { 75 | "multiline": true, 76 | "singleline": "never" 77 | } 78 | ], 79 | 80 | "align": false, 81 | "class-name": true, 82 | "comment-format": [ 83 | true, 84 | "check-space" 85 | ], 86 | "interface-name": false, 87 | "jsdoc-format": true, 88 | "no-consecutive-blank-lines": false, 89 | "one-line": [ 90 | true, 91 | "check-open-brace", 92 | "check-catch", 93 | "check-else", 94 | "check-finally", 95 | "check-whitespace" 96 | ], 97 | "quotemark": [ 98 | true, 99 | "single", 100 | "avoid-escape" 101 | ], 102 | "semicolon": [true, "always"], 103 | "variable-name": [ 104 | true, 105 | "check-format", 106 | "allow-leading-underscore", 107 | "allow-pascal-case", 108 | "ban-keywords" 109 | ], 110 | "whitespace": [ 111 | true, 112 | "check-branch", 113 | "check-decl", 114 | "check-operator", 115 | "check-separator", 116 | "check-type" 117 | ] 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GRPC Bus [![codecov](https://codecov.io/gh/paralin/grpc-bus/branch/master/graph/badge.svg)](https://codecov.io/gh/paralin/grpc-bus) [![Build Status](https://travis-ci.org/paralin/grpc-bus.svg?branch=master)](https://travis-ci.org/paralin/grpc-bus) [![npm version](https://badge.fury.io/js/grpc-bus.svg)](https://badge.fury.io/js/grpc-bus) [![dependencies Status](https://david-dm.org/paralin/grpc-bus/status.svg)](https://david-dm.org/paralin/grpc-bus) [![devDependencies Status](https://david-dm.org/paralin/grpc-bus/dev-status.svg)](https://david-dm.org/paralin/grpc-bus?type=dev) 2 | ======== 3 | 4 | **NOTE:** Development on grpc-bus is currently suspended as I have transitioned to Go from Node in all of my projects. If you want to adopt the project, let me know! 5 | 6 | GRPC-bus is a mechanism to call GRPC services from the browser using a Node.JS server as a proxy. The link between the browser and Node.JS is defined by the user, but could be something like a WebSocket. 7 | 8 | The server and client are expected to share the same / similar protobuf tree. For example, the same result should come from the following code on both the client and server: 9 | 10 | ```js 11 | builder.Build("mynamespace.MyType"); 12 | ``` 13 | 14 | In this way the client can implement the ProtoBuf.JS RPC interfaces in the browser. Then, the grpc-bus package does the following: 15 | 16 | - Keep track of connections to desired remote servers 17 | - Make service calls on behalf of the client 18 | - Keep track of streaming calls and pipe these back to the client accordingly. 19 | 20 | Thus, we can call GRPC servers from the browser via a Node.JS websocket stream. 21 | 22 | Example 23 | ======= 24 | 25 | A full example can be found in the end-to-end tests under `./src/index.spec.ts`. 26 | 27 | First, create your client, and give it a way to communicate with the server: 28 | 29 | ```js 30 | var protoTree = ProtobufJS.load('...'); 31 | var grpcBus = require('grpc-bus'); 32 | // MySendFunction takes a message object. 33 | // This message should be passed to handleMessage on the server. 34 | var client = new grpcBus.Client(protoTree, mySendFunction); 35 | var tree = client.buildTree(); 36 | tree.MyService('localhost:3000').then(function(service) { 37 | service.MyMethod({hello: 'world'}, function(err, resp) { 38 | console.log(resp); 39 | service.end(); 40 | }); 41 | }); 42 | ``` 43 | 44 | You should always call `service.end()` when you are done with a service handle, so the server knows it's safe to dispose it. 45 | 46 | You'll notice that inside the `then` block the API is exactly the same as the Node GRPC api. 47 | 48 | Internals 49 | ========= 50 | 51 | A client must first be instantiated. The client object has to be given a function to send a message to the server, and should be called when the server sends a message to it. In this way, the user can implement their own transport, for example, websockets. 52 | 53 | Next, the client can instantiate a service object, similar to the GRPC Node API. This returns a promise, resolved with a service handle with stubs for the methods on the service. The server will de-duplicate and re-use multiple service objects internally. 54 | 55 | The client can then make calls against the remote service with the same API as the GRPC Node implementation. 56 | 57 | When the client is done with a service object, it should dispose it. When all service objects are disposed, the server will disconnect from the service and forget the credentials used. 58 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Client, 3 | Server, 4 | IGBClientMessage, 5 | IGBServerMessage, 6 | IServiceHandle, 7 | IGRPCTree, 8 | } from './index'; 9 | import { buildTree, MockServer } from './mock'; 10 | 11 | let grpc = require('grpc'); 12 | let mockProtoDefs = buildTree(); 13 | let mockProto = grpc.loadObject(mockProtoDefs.lookup('')); 14 | 15 | describe('e2e', () => { 16 | let grpcServer: any; 17 | let mockServer: MockServer; 18 | 19 | let gbClient: Client; 20 | let gbServer: Server; 21 | 22 | let gbClientService: IServiceHandle; 23 | let gbTree: IGRPCTree; 24 | 25 | beforeEach((done) => { 26 | grpcServer = new grpc.Server(); 27 | mockServer = new MockServer(); 28 | grpcServer.addProtoService(mockProto.mock.Greeter.service, mockServer); 29 | grpcServer.bind('0.0.0.0:50053', grpc.ServerCredentials.createInsecure()); 30 | grpcServer.start(); 31 | 32 | gbClient = new Client(mockProtoDefs, (msg: IGBClientMessage) => { 33 | gbServer.handleMessage(msg); 34 | }); 35 | gbServer = new Server(mockProtoDefs, (msg: IGBServerMessage) => { 36 | gbClient.handleMessage(msg); 37 | }, require('grpc')); 38 | 39 | gbTree = gbClient.buildTree(); 40 | let svcPromise: Promise = gbTree['mock']['Greeter']('localhost:50053'); 41 | svcPromise.then((svc) => { 42 | gbClientService = svc; 43 | done(); 44 | }, done); 45 | }); 46 | 47 | afterEach(() => { 48 | gbClientService.end(); 49 | grpcServer.forceShutdown(); 50 | mockServer = grpcServer = null; 51 | gbClient.reset(); 52 | gbServer.dispose(); 53 | }); 54 | 55 | it('should make a non-streaming call properly', (done) => { 56 | mockServer.callReceived.subscribe((call: string) => { 57 | expect(call).toBe('sayHello'); 58 | }); 59 | gbClientService['sayHello']({name: 'kappa'}, (err: any, res: any) => { 60 | if (err) { 61 | throw err; 62 | } 63 | done(); 64 | }); 65 | }, 5000); 66 | 67 | it('should make a client-side streaming call properly', (done) => { 68 | mockServer.callReceived.subscribe((call: string) => { 69 | expect(call).toBe('sayHelloClientStream'); 70 | }); 71 | let call = gbClientService['sayHelloClientStream']((err: any, res: any) => { 72 | if (err) { 73 | throw err; 74 | } 75 | done(); 76 | }); 77 | call.write({name: 'FailFish'}); 78 | call.end(); 79 | }, 5000); 80 | 81 | it('should make a bidirectional streaming call properly', (done) => { 82 | mockServer.callReceived.subscribe((call: string) => { 83 | expect(call).toBe('sayHelloBidiStream'); 84 | }); 85 | let call = gbClientService['sayHelloBidiStream'](); 86 | call.on('data', (data) => { 87 | // buggy expect 88 | expect(data.toRaw()).toEqual({message: 'FailFish'}); 89 | }); 90 | call.on('error', done); 91 | call.on('end', () => { 92 | done(); 93 | }); 94 | call.write({name: 'FailFish'}); 95 | call.end(); 96 | }, 5000); 97 | 98 | it('should make a server-side streaming call properly', (done) => { 99 | mockServer.callReceived.subscribe((call: string) => { 100 | expect(call).toBe('sayHelloServerStream'); 101 | }); 102 | let call = gbClientService['sayHelloServerStream']({name: 'FailFish'}); 103 | call.on('data', (data) => { 104 | expect(data.toRaw()).toEqual({message: 'Hello'}); 105 | }); 106 | call.on('error', (err) => { 107 | throw err; 108 | }); 109 | call.on('end', () => { 110 | done(); 111 | }); 112 | }, 5000); 113 | }); 114 | -------------------------------------------------------------------------------- /proto/grpc-bus.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package grpcbus; 3 | 4 | // Wrapper for a message from client 5 | message GBClientMessage { 6 | // Service management 7 | GBCreateService service_create = 1; 8 | GBReleaseService service_release = 2; 9 | 10 | // Call management 11 | GBCreateCall call_create = 3; 12 | // Terminates a call 13 | GBCallEnd call_end = 4; 14 | GBSendCall call_send = 5; 15 | } 16 | 17 | message GBServerMessage { 18 | // service management responses 19 | GBCreateServiceResult service_create = 1; 20 | GBReleaseServiceResult service_release = 2; 21 | 22 | // Call management 23 | GBCreateCallResult call_create = 3; 24 | GBCallEvent call_event = 4; 25 | GBCallEnded call_ended = 5; 26 | } 27 | 28 | // Information about a service 29 | message GBServiceInfo { 30 | // Endpoint 31 | string endpoint = 1; 32 | // Fully qualified service identifier 33 | string service_id = 2; 34 | // TODO: figure out how to serialize credentials 35 | } 36 | 37 | // Initialize a new Service. 38 | message GBCreateService { 39 | // ID of the service, client-generated, unique. 40 | int32 service_id = 1; 41 | GBServiceInfo service_info = 2; 42 | } 43 | 44 | // Release an existing / pending Service. 45 | message GBReleaseService { 46 | int32 service_id = 1; 47 | } 48 | 49 | message GBCallInfo { 50 | string method_id = 1; 51 | bytes bin_argument = 2; 52 | } 53 | 54 | // Create a call 55 | message GBCreateCall { 56 | int32 service_id = 1; 57 | int32 call_id = 2; 58 | // Info 59 | GBCallInfo info = 3; 60 | } 61 | 62 | // When the call is ended 63 | message GBCallEnded { 64 | int32 call_id = 1; 65 | int32 service_id = 2; 66 | } 67 | 68 | // End the call 69 | message GBEndCall { 70 | int32 call_id = 1; 71 | int32 service_id = 2; 72 | } 73 | 74 | // Send a message on a streaming call 75 | message GBSendCall { 76 | int32 call_id = 1; 77 | int32 service_id = 2; 78 | bytes bin_data = 3; 79 | // Do we want to just send end() over a streaming call? 80 | bool is_end = 4; 81 | } 82 | 83 | // Result of attempting to create a service 84 | message GBCreateServiceResult { 85 | // ID of service, client-generated, unique 86 | int32 service_id = 1; 87 | // Result 88 | ECreateServiceResult result = 2; 89 | // Error details 90 | string error_details = 3; 91 | 92 | enum ECreateServiceResult { 93 | // Success 94 | SUCCESS = 0; 95 | // Invalid service ID, retry with a new one. 96 | INVALID_ID = 1; 97 | // GRPC internal error constructing the service. 98 | GRPC_ERROR = 2; 99 | } 100 | } 101 | 102 | // When the server releases a service 103 | message GBReleaseServiceResult { 104 | int32 service_id = 1; 105 | } 106 | 107 | // Result of creating a call. 108 | // This is sent immediately after starting call. 109 | message GBCreateCallResult { 110 | int32 call_id = 1; 111 | int32 service_id = 4; 112 | 113 | // Result 114 | ECreateCallResult result = 2; 115 | string error_details = 3; 116 | 117 | enum ECreateCallResult { 118 | // Success 119 | SUCCESS = 0; 120 | // Invalid call ID, retry with a new one. 121 | INVALID_ID = 1; 122 | // GRPC internal error initializing the call 123 | GRPC_ERROR = 2; 124 | } 125 | } 126 | 127 | // Received message during streaming call. 128 | message GBCallEvent { 129 | // Call ID 130 | int32 call_id = 1; 131 | // Service ID 132 | int32 service_id = 4; 133 | // Event ID 134 | string event = 2; 135 | // JSON data. 136 | string json_data = 3; 137 | // Binary data 138 | bytes bin_data = 5; 139 | } 140 | 141 | // Terminate a call 142 | message GBCallEnd { 143 | int32 call_id = 1; 144 | int32 service_id = 2; 145 | } 146 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import { ServiceStore } from './store'; 2 | import { CallStore } from './call_store'; 3 | import { 4 | IGBServerMessage, 5 | IGBClientMessage, 6 | IGBSendCall, 7 | IGBCreateService, 8 | IGBCreateServiceResult, 9 | IGBReleaseService, 10 | IGBCreateCall, 11 | IGBEndCall, 12 | } from '../proto'; 13 | 14 | // A server for a remote client. 15 | export class Server { 16 | // Store of remote services 17 | // tslint:disable-next-line 18 | private store: ServiceStore; 19 | 20 | // Map of known client IDs to services. 21 | private clientIdToService: { [id: number]: CallStore } = {}; 22 | 23 | public constructor(private protoTree: any, 24 | private send: (message: IGBServerMessage) => void, 25 | // Pass require('grpc') 26 | private grpc: any) { 27 | this.store = new ServiceStore(protoTree, this.grpc); 28 | } 29 | 30 | public handleMessage(message: IGBClientMessage) { 31 | if (message.service_create) { 32 | this.handleServiceCreate(message.service_create); 33 | } 34 | if (message.service_release) { 35 | this.handleServiceRelease(message.service_release); 36 | } 37 | if (message.call_create) { 38 | this.handleCallCreate(message.call_create); 39 | } 40 | if (message.call_end) { 41 | this.handleCallEnd(message.call_end); 42 | } 43 | if (message.call_send) { 44 | this.handleCallSend(message.call_send); 45 | } 46 | } 47 | 48 | public dispose() { 49 | for (let servId in this.clientIdToService) { 50 | if (!this.clientIdToService.hasOwnProperty(servId)) { 51 | continue; 52 | } 53 | this.clientIdToService[servId].dispose(); 54 | } 55 | this.clientIdToService = {}; 56 | } 57 | 58 | private releaseLocalService(serviceId: number, sendGratuitous: boolean = true) { 59 | let srv = this.clientIdToService[serviceId]; 60 | if (srv) { 61 | sendGratuitous = true; 62 | // Kill all ongoing calls, inform the client they are ended 63 | delete this.clientIdToService[serviceId]; 64 | srv.dispose(); 65 | } 66 | if (sendGratuitous) { 67 | // Inform the client the service has been released 68 | this.send({ 69 | service_release: { 70 | service_id: serviceId, 71 | }, 72 | }); 73 | } 74 | } 75 | 76 | private handleServiceCreate(msg: IGBCreateService) { 77 | let serviceId = msg.service_id; 78 | let result: IGBCreateServiceResult = { 79 | service_id: msg.service_id, 80 | result: 0, 81 | }; 82 | if (typeof msg.service_id !== 'number' || this.clientIdToService[msg.service_id]) { 83 | // todo: fix enums 84 | result.result = 1; 85 | result.error_details = 'ID is not set or is already in use.'; 86 | } else { 87 | try { 88 | let serv = this.store.getService(msg.service_id, msg.service_info); 89 | // Here, we may get an error thrown if the info is invalid. 90 | serv.initStub(); 91 | // When the service is disposed, also dispose the client service. 92 | serv.disposed.subscribe(() => { 93 | this.releaseLocalService(serviceId, false); 94 | }); 95 | this.clientIdToService[serviceId] = new CallStore(serv, msg.service_id, this.send); 96 | } catch (e) { 97 | result.result = 2; 98 | result.error_details = e.toString(); 99 | } 100 | } 101 | 102 | this.send({ 103 | service_create: result, 104 | }); 105 | } 106 | 107 | private handleServiceRelease(msg: IGBReleaseService) { 108 | this.releaseLocalService(msg.service_id); 109 | } 110 | 111 | private handleCallSend(msg: IGBSendCall) { 112 | let svc = this.clientIdToService[msg.service_id]; 113 | if (!svc) { 114 | this.releaseLocalService(msg.service_id, true); 115 | return; 116 | } 117 | svc.handleCallWrite(msg); 118 | } 119 | 120 | private handleCallCreate(msg: IGBCreateCall) { 121 | let svc = this.clientIdToService[msg.service_id]; 122 | if (!svc) { 123 | this.send({ 124 | call_create: { 125 | result: 1, 126 | service_id: msg.service_id, 127 | call_id: msg.call_id, 128 | error_details: 'Service ID not found.', 129 | }, 130 | }); 131 | return; 132 | } 133 | svc.initCall(msg); 134 | } 135 | 136 | private handleCallEnd(msg: IGBEndCall) { 137 | let svc = this.clientIdToService[msg.service_id]; 138 | if (svc) { 139 | svc.handleCallEnd(msg); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/client/call.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IGBCallInfo, 3 | IGBCreateCallResult, 4 | IGBCallEvent, 5 | IGBCallEnded, 6 | IGBClientMessage, 7 | } from '../proto'; 8 | import { Subject } from 'rxjs/Subject'; 9 | 10 | export interface ICallHandle { 11 | // Send a message on a streaming call 12 | write?(msg: any): void; 13 | 14 | // Register a callback handler on a streaming call. 15 | on?(eventId: string, callback: (arg: any) => void): void; 16 | 17 | // Remove all handlers for an event on a streaming call. 18 | off?(eventId: string): void; 19 | 20 | // Call to send the 'end' on a client-side streaming call. 21 | end?(): void; 22 | 23 | // Call to terminate this call on a streaming call. 24 | terminate?(): void; 25 | } 26 | 27 | export class Call implements ICallHandle { 28 | public disposed: Subject = new Subject(); 29 | private eventHandlers: { [id: string]: ((arg: any) => void)[] } = {}; 30 | private endEmitted: boolean = false; 31 | private responseBuilder: any; 32 | private requestBuilder: any; 33 | 34 | constructor(public clientId: number, 35 | public clientServiceId: number, 36 | private info: IGBCallInfo, 37 | private callMeta: any, 38 | private callback: (error?: any, response?: any) => void, 39 | private send: (message: IGBClientMessage) => void) { 40 | this.requestBuilder = callMeta.resolvedRequestType.build(); 41 | this.responseBuilder = callMeta.resolvedResponseType.build(); 42 | } 43 | 44 | public on(eventId: string, callback: (arg: any) => void): void { 45 | let handlers = this.eventHandlers[eventId]; 46 | if (!handlers) { 47 | handlers = []; 48 | this.eventHandlers[eventId] = handlers; 49 | } 50 | handlers.push(callback); 51 | } 52 | 53 | public off(eventId: string) { 54 | delete this.eventHandlers[eventId]; 55 | } 56 | 57 | public handleCreateResponse(msg: IGBCreateCallResult) { 58 | if (msg.result === 0) { 59 | return; 60 | } 61 | if (msg.error_details && msg.error_details.length) { 62 | this.terminateWithError(msg.error_details); 63 | } else { 64 | this.terminateWithError('Error ' + msg.result); 65 | } 66 | } 67 | 68 | public handleEnded(msg: IGBCallEnded) { 69 | this.dispose(); 70 | } 71 | 72 | public handleEvent(msg: IGBCallEvent) { 73 | let data: any = null; 74 | if (msg.json_data && msg.json_data.length) { 75 | data = JSON.parse(msg.json_data); 76 | } else if (msg.bin_data && (msg.bin_data.limit || msg.bin_data.length)) { 77 | data = this.decodeResponseData(msg.bin_data); 78 | } 79 | this.emit(msg.event, data); 80 | if (this.callback) { 81 | if (msg.event === 'error') { 82 | this.terminateWithError(data); 83 | } else if (msg.event === 'data') { 84 | this.terminateWithData(data); 85 | } 86 | } 87 | } 88 | 89 | public end() { 90 | this.send({ 91 | call_send: { 92 | call_id: this.clientId, 93 | service_id: this.clientServiceId, 94 | is_end: true, 95 | }, 96 | }); 97 | } 98 | 99 | public terminate() { 100 | this.send({ 101 | call_end: { 102 | call_id: this.clientId, 103 | service_id: this.clientServiceId, 104 | }, 105 | }); 106 | this.dispose(); 107 | } 108 | 109 | public write(msg: any) { 110 | if (!this.callMeta.requestStream) { 111 | throw new Error('Cannot write to a non-streaming request.'); 112 | } 113 | if (typeof msg !== 'object') { 114 | throw new Error('Can only write objects to streaming requests.'); 115 | } 116 | this.send({ 117 | call_send: { 118 | call_id: this.clientId, 119 | service_id: this.clientServiceId, 120 | bin_data: this.requestBuilder.encode(msg), 121 | }, 122 | }); 123 | } 124 | 125 | public dispose() { 126 | if (!this.endEmitted) { 127 | this.emit('end', null); 128 | } 129 | this.disposed.next(this); 130 | } 131 | 132 | private decodeResponseData(data: any): any { 133 | return this.responseBuilder.decode(data); 134 | } 135 | 136 | private terminateWithError(error: any) { 137 | if (this.callback) { 138 | this.callback(error, null); 139 | } else { 140 | this.emit('error', error); 141 | } 142 | this.dispose(); 143 | } 144 | 145 | private terminateWithData(data: any) { 146 | this.callback(null, data); 147 | this.dispose(); 148 | } 149 | 150 | private emit(eventId: string, arg: any) { 151 | if (eventId === 'end') { 152 | this.endEmitted = true; 153 | } 154 | 155 | let handlers = this.eventHandlers[eventId]; 156 | if (!handlers) { 157 | return; 158 | } 159 | for (let handler of handlers) { 160 | handler(arg); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/server/call.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs/Subject'; 2 | import { Service } from './service'; 3 | import { 4 | IGBCallInfo, 5 | IGBCallEvent, 6 | IGBServerMessage, 7 | } from '../proto'; 8 | 9 | import * as _ from 'lodash'; 10 | 11 | // An ongoing call against a service. 12 | export class Call { 13 | // Subject called when disposed. 14 | public disposed: Subject = new Subject(); 15 | // Handle returned by a client-side streaming call. 16 | private streamHandle: any; 17 | private rpcMeta: any; 18 | 19 | public constructor(private service: Service, 20 | private clientId: number, 21 | private clientServiceId: number, 22 | private callInfo: IGBCallInfo, 23 | private send: (msg: IGBServerMessage) => void) { 24 | } 25 | 26 | public initCall() { 27 | if (!this.callInfo || !this.callInfo.method_id) { 28 | throw new Error('Call info, method ID must be given'); 29 | } 30 | let args: any = this.callInfo.bin_argument; 31 | let rpcMeta = this.service.lookupMethod(this.callInfo.method_id); 32 | if (!rpcMeta) { 33 | throw new Error('Method ' + this.callInfo.method_id + ' not found.'); 34 | } 35 | this.rpcMeta = rpcMeta; 36 | if (rpcMeta.className !== 'Service.RPCMethod') { 37 | throw new Error('Method ' + 38 | this.callInfo.method_id + 39 | ' is a ' + 40 | rpcMeta.className + 41 | ' not a Service.RPCMethod'); 42 | } 43 | let camelMethod = _.camelCase(rpcMeta.name); 44 | if (!this.service.stub[camelMethod] || typeof this.service.stub[camelMethod] !== 'function') { 45 | throw new Error('Method ' + camelMethod + ' not defined by grpc.'); 46 | } 47 | if (rpcMeta.requestStream && !rpcMeta.responseStream) { 48 | this.streamHandle = this.service.stub[camelMethod]((error: any, response: any) => { 49 | this.handleCallCallback(error, response); 50 | }); 51 | // If they sent some args (shouldn't happen usually) send it off anyway 52 | if (args) { 53 | this.streamHandle.write(args); 54 | } 55 | } else if (rpcMeta.requestStream && rpcMeta.responseStream) { 56 | this.streamHandle = this.service.stub[camelMethod](); 57 | this.setCallHandlers(this.streamHandle); 58 | } else if (!rpcMeta.requestStream && rpcMeta.responseStream) { 59 | this.streamHandle = this.service.stub[camelMethod](args); 60 | this.setCallHandlers(this.streamHandle); 61 | } else if (!rpcMeta.requestStream && !rpcMeta.responseStream) { 62 | if (!args) { 63 | throw new Error('Method ' + 64 | this.callInfo.method_id + 65 | ' requires an argument object of type ' + 66 | rpcMeta.requestName + '.'); 67 | } 68 | this.service.stub[camelMethod](args, (error: any, response: any) => { 69 | this.handleCallCallback(error, response); 70 | }); 71 | } 72 | } 73 | 74 | public write(msg: any) { 75 | if (!this.rpcMeta.requestStream || 76 | !this.streamHandle || 77 | typeof this.streamHandle['write'] !== 'function') { 78 | return; 79 | } 80 | this.streamHandle.write(msg); 81 | } 82 | 83 | public sendEnd() { 84 | if (!this.rpcMeta.requestStream || 85 | !this.streamHandle || 86 | typeof this.streamHandle['end'] !== 'function') { 87 | return; 88 | } 89 | this.streamHandle.end(); 90 | } 91 | 92 | public dispose() { 93 | this.send({ 94 | call_ended: { 95 | call_id: this.clientId, 96 | service_id: this.clientServiceId, 97 | }, 98 | }); 99 | if (this.streamHandle && typeof this.streamHandle['end'] === 'function') { 100 | this.streamHandle.end(); 101 | this.streamHandle = null; 102 | } 103 | this.disposed.next(this); 104 | } 105 | 106 | private handleCallCallback(error: any, response: any) { 107 | if (error) { 108 | this.callEventHandler('error')(error); 109 | } 110 | if (response) { 111 | this.callEventHandler('data', true)(response); 112 | } 113 | this.dispose(); 114 | } 115 | 116 | private setCallHandlers(streamHandle: any) { 117 | let dataHandler = this.callEventHandler('data', true); 118 | this.streamHandle.on('data', (data: any) => { 119 | dataHandler(data); 120 | }); 121 | this.streamHandle.on('status', this.callEventHandler('status')); 122 | this.streamHandle.on('error', this.callEventHandler('error')); 123 | this.streamHandle.on('end', this.callEventHandler('end')); 124 | } 125 | 126 | private callEventHandler(eventId: string, isBin: boolean = false) { 127 | return (data: any) => { 128 | let callEvent: IGBCallEvent = { 129 | service_id: this.clientServiceId, 130 | call_id: this.clientId, 131 | json_data: !isBin ? JSON.stringify(data) : undefined, 132 | bin_data: isBin ? data : undefined, 133 | event: eventId, 134 | }; 135 | if (!callEvent.json_data) { 136 | delete callEvent.json_data; 137 | } 138 | if (!callEvent.bin_data) { 139 | delete callEvent.bin_data; 140 | } 141 | this.send({ 142 | call_event: callEvent, 143 | }); 144 | }; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/client/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IGBServerMessage, 3 | IGBClientMessage, 4 | IGBReleaseServiceResult, 5 | IGBCreateCallResult, 6 | IGBCallEnded, 7 | IGBCallEvent, 8 | IGBCreateServiceResult, 9 | IGBServiceInfo, 10 | } from '../proto'; 11 | import { Service } from './service'; 12 | import { IServiceHandle } from './service'; 13 | 14 | export interface IGRPCTree { 15 | [id: string]: IGRPCTree | ((endpoint: string) => Promise); 16 | } 17 | 18 | // Hack: see if something is *probably* a namespace. 19 | // This is due to the extreme bugginess around namespaces in pbjs. 20 | function likelyNamespace(tree: any): boolean { 21 | if (tree.className === 'Namespace') { 22 | return true; 23 | } 24 | if (tree.className !== 'Message' || 25 | !tree.children || 26 | tree.children.length < 1) { 27 | return false; 28 | } 29 | for (let child of tree.children) { 30 | // Messages never have services as children. 31 | if (child.className === 'Service') { 32 | return true; 33 | } 34 | // Namespaces never have fields as children. 35 | if (child.className === 'Message.Field') { 36 | return false; 37 | } 38 | } 39 | } 40 | 41 | export class Client { 42 | private serviceIdCounter: number = 1; 43 | private services: { [id: number]: Service } = {}; 44 | 45 | public constructor(private protoTree: any, 46 | private send: (message: IGBClientMessage) => void) { 47 | } 48 | 49 | public handleMessage(message: IGBServerMessage) { 50 | if (message.service_create) { 51 | this.handleServiceCreate(message.service_create); 52 | } 53 | if (message.call_create) { 54 | this.handleCallCreate(message.call_create); 55 | } 56 | if (message.call_event) { 57 | this.handleCallEvent(message.call_event); 58 | } 59 | if (message.call_ended) { 60 | this.handleCallEnded(message.call_ended); 61 | } 62 | if (message.service_release) { 63 | this.handleServiceRelease(message.service_release); 64 | } 65 | } 66 | 67 | public buildTree(base: string = '') { 68 | let meta = this.protoTree.lookup(base); 69 | if (!meta) { 70 | throw new Error('Base identifier ' + base + ' not found.'); 71 | } 72 | return this.recurseBuildTree(meta, base); 73 | } 74 | 75 | // Clears all ongoing calls + services, etc 76 | public reset() { 77 | for (let serviceId in this.services) { 78 | /* istanbul ignore next */ 79 | if (!this.services.hasOwnProperty(serviceId)) { 80 | continue; 81 | } 82 | let service = this.services[serviceId]; 83 | service.end(); 84 | } 85 | this.services = {}; 86 | } 87 | 88 | private recurseBuildTree(tree: any, identifier: string): IGRPCTree | 89 | ((endpoint: string) => Promise) { 90 | let result: IGRPCTree = {}; 91 | let nextIdentifier: string = tree.name; 92 | if (identifier.length) { 93 | nextIdentifier = identifier + '.' + nextIdentifier; 94 | } 95 | // Bit of a hack here, detect namespace several ways. 96 | if (likelyNamespace(tree)) { 97 | for (let child of tree.children) { 98 | result[child.name] = this.recurseBuildTree(child, nextIdentifier); 99 | } 100 | return result; 101 | } 102 | if (tree.className === 'Service') { 103 | return (endpoint: string) => { 104 | return this.buildService(nextIdentifier, endpoint); 105 | }; 106 | } 107 | if (tree.className === 'Message' || tree.className === 'Enum') { 108 | return tree.build(); 109 | } 110 | return tree; 111 | } 112 | 113 | // Build a service and return a service handle promise. 114 | private buildService(method: string, endpoint: string): Promise { 115 | return new Promise((resolve, reject) => { 116 | let sid = this.serviceIdCounter++; 117 | let info: IGBServiceInfo = { 118 | service_id: method, 119 | endpoint: endpoint, 120 | }; 121 | let service = new Service(this.protoTree, sid, info, { 122 | resolve: resolve, 123 | reject: reject, 124 | }, this.send); 125 | service.initStub(); 126 | this.services[sid] = service; 127 | service.disposed.subscribe(() => { 128 | delete this.services[sid]; 129 | }); 130 | this.send({ 131 | service_create: { 132 | service_id: sid, 133 | service_info: info, 134 | }, 135 | }); 136 | }); 137 | } 138 | 139 | private handleServiceCreate(msg: IGBCreateServiceResult) { 140 | let service = this.services[msg.service_id]; 141 | if (!service) { 142 | return; 143 | } 144 | service.handleCreateResponse(msg); 145 | } 146 | 147 | private handleCallCreate(msg: IGBCreateCallResult) { 148 | let service = this.services[msg.service_id]; 149 | if (!service) { 150 | return; 151 | } 152 | service.handleCallCreateResponse(msg); 153 | } 154 | 155 | private handleServiceRelease(msg: IGBReleaseServiceResult) { 156 | let svc = this.services[msg.service_id]; 157 | if (!svc) { 158 | return; 159 | } 160 | svc.handleServiceRelease(msg); 161 | } 162 | 163 | private handleCallEnded(msg: IGBCallEnded) { 164 | let svc = this.services[msg.service_id]; 165 | if (!svc) { 166 | return; 167 | } 168 | svc.handleCallEnded(msg); 169 | } 170 | 171 | private handleCallEvent(msg: IGBCallEvent) { 172 | let service = this.services[msg.service_id]; 173 | if (!service) { 174 | return; 175 | } 176 | service.handleCallEvent(msg); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/server/server.spec.ts: -------------------------------------------------------------------------------- 1 | import { Server } from './server'; 2 | import { 3 | IGBServiceInfo, 4 | IGBServerMessage, 5 | } from '../proto'; 6 | import { 7 | buildTree, 8 | } from '../mock'; 9 | 10 | describe('Server', () => { 11 | let server: Server; 12 | let serviceInfo: IGBServiceInfo = { 13 | endpoint: 'localhost:5000', 14 | service_id: 'mock.Greeter', 15 | }; 16 | let recvQueue: IGBServerMessage[] = []; 17 | let lookupTree: any; 18 | // An encoded sample request 19 | let encodedData: any; 20 | // An encoded sample response 21 | let encodedResponseData: any; 22 | 23 | beforeEach(() => { 24 | lookupTree = buildTree(); 25 | server = new Server(lookupTree, (msg: IGBServerMessage) => { 26 | recvQueue.push(msg); 27 | }, require('grpc')); 28 | recvQueue.length = 0; 29 | let serviceTree = require('grpc').loadObject(lookupTree).build(); 30 | encodedData = serviceTree.mock.HelloRequest.encode({'name': 'hello'}).toBase64(); 31 | encodedResponseData = serviceTree.mock.HelloReply.encode({message: 'hello'}).toBase64(); 32 | }); 33 | 34 | it('should create a service correctly', () => { 35 | server.handleMessage({service_create: { 36 | service_id: 1, 37 | service_info: serviceInfo, 38 | }}); 39 | expect(recvQueue.length).toBe(1); 40 | let msg = recvQueue.splice(0, 1)[0]; 41 | expect(msg.service_create).not.toBe(null); 42 | if (msg.service_create.error_details) { 43 | throw new Error(msg.service_create.error_details); 44 | } 45 | expect(msg.service_create.result).toBe(0); 46 | 47 | expect(recvQueue.length).toBe(0); 48 | server.handleMessage({service_release: { 49 | service_id: 1, 50 | }}); 51 | expect(recvQueue.length).toBe(1); 52 | msg = recvQueue.splice(0, 1)[0]; 53 | expect(msg.service_release).not.toBe(null); 54 | expect(msg.service_release.service_id).toBe(1); 55 | }); 56 | 57 | it('should respond with gratuitous releases', () => { 58 | server.handleMessage({service_release: { 59 | service_id: 50, 60 | }}); 61 | expect(recvQueue.length).toBe(1); 62 | let msg = recvQueue.splice(0, 1)[0]; 63 | expect(msg.service_release).not.toBe(null); 64 | expect(msg.service_release.service_id).toBe(50); 65 | }); 66 | 67 | it('should filter empty service ids', () => { 68 | server.handleMessage({service_create: {}}); 69 | expect(recvQueue.length).toBe(1); 70 | let msg = recvQueue.splice(0, 1)[0]; 71 | expect(msg.service_create).not.toBe(null); 72 | expect(msg.service_create.error_details).toBe('ID is not set or is already in use.'); 73 | }); 74 | 75 | it('should filter invalid service ids', () => { 76 | server.handleMessage({service_create: { 77 | service_id: 1, 78 | service_info: { 79 | endpoint: 'localhost:3000', 80 | service_id: 'mock', 81 | }, 82 | }}); 83 | expect(recvQueue.length).toBe(1); 84 | let msg = recvQueue.splice(0, 1)[0]; 85 | expect(msg.service_create).not.toBe(null); 86 | expect(msg.service_create.error_details).toBe('TypeError: mock is a Namespace not a Service.'); 87 | }); 88 | 89 | it('should filter unknown service ids', () => { 90 | server.handleMessage({service_create: { 91 | service_id: 1, 92 | service_info: { 93 | endpoint: 'localhost:3000', 94 | service_id: 'mock.wow.NotExist', 95 | }, 96 | }}); 97 | expect(recvQueue.length).toBe(1); 98 | let msg = recvQueue.splice(0, 1)[0]; 99 | expect(msg.service_create).not.toBe(null); 100 | expect(msg.service_create.error_details).toBe('TypeError: mock.wow.NotExist was not found.'); 101 | }); 102 | 103 | it('should start a call correctly', () => { 104 | server.handleMessage({service_create: { 105 | service_id: 1, 106 | service_info: { 107 | endpoint: 'localhost:3000', 108 | service_id: 'mock.Greeter', 109 | }, 110 | }}); 111 | recvQueue.length = 0; 112 | server.handleMessage({ 113 | call_create: { 114 | call_id: 1, 115 | service_id: 1, 116 | info: { 117 | method_id: 'SayHello', 118 | bin_argument: encodedData, 119 | }, 120 | }, 121 | }); 122 | expect(recvQueue).toEqual([{ 123 | call_create: { 124 | call_id: 1, 125 | service_id: 1, 126 | result: 0, 127 | }, 128 | }]); 129 | recvQueue.length = 0; 130 | }); 131 | 132 | it('should start a streaming call correctly', () => { 133 | server.handleMessage({service_create: { 134 | service_id: 1, 135 | service_info: { 136 | endpoint: 'localhost:3000', 137 | service_id: 'mock.Greeter', 138 | }, 139 | }}); 140 | recvQueue.length = 0; 141 | server.handleMessage({ 142 | call_create: { 143 | call_id: 1, 144 | service_id: 1, 145 | info: { 146 | method_id: 'SayHelloBidiStream', 147 | }, 148 | }, 149 | }); 150 | expect(recvQueue).toEqual([{ 151 | call_create: { 152 | call_id: 1, 153 | service_id: 1, 154 | result: 0, 155 | }, 156 | }]); 157 | recvQueue.length = 0; 158 | server.handleMessage({ 159 | call_send: { 160 | call_id: 1, 161 | service_id: 1, 162 | bin_data: encodedData, 163 | }, 164 | }); 165 | expect(recvQueue.length).toBe(0); 166 | }); 167 | 168 | it('should dispose properly', () => { 169 | server.handleMessage({service_create: { 170 | service_id: 1, 171 | service_info: { 172 | endpoint: 'localhost:3000', 173 | service_id: 'mock.Greeter', 174 | }, 175 | }}); 176 | server.handleMessage({ 177 | call_create: { 178 | call_id: 1, 179 | info: { 180 | method_id: 'SayHelloBidiStream', 181 | }, 182 | service_id: 1, 183 | }, 184 | }); 185 | recvQueue.length = 0; 186 | server.dispose(); 187 | expect(recvQueue).toEqual([{ 188 | call_ended: { 189 | call_id: 1, 190 | service_id: 1, 191 | }, 192 | }, { 193 | service_release: { 194 | service_id: 1, 195 | }, 196 | }]); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /src/client/service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IGBClientMessage, 3 | IGBServiceInfo, 4 | IGBCreateCallResult, 5 | IGBCallEnded, 6 | IGBCallEvent, 7 | IGBCreateServiceResult, 8 | IGBReleaseServiceResult, 9 | IGBCallInfo, 10 | } from '../proto'; 11 | import { 12 | Subject, 13 | } from 'rxjs/Subject'; 14 | import { Call, ICallHandle } from './call'; 15 | 16 | export interface IServicePromise { 17 | resolve: (handle: IServiceHandle) => void; 18 | reject: (error?: any) => void; 19 | } 20 | 21 | export interface IServiceHandle { 22 | // Call when done using this service. 23 | end(): ICallHandle; 24 | 25 | // All the available stub calls 26 | [id: string]: (argument?: any, 27 | callback?: (error?: any, response?: any) => void) 28 | => ICallHandle; 29 | } 30 | 31 | export class Service { 32 | public disposed: Subject = new Subject(); 33 | public handle: IServiceHandle; 34 | private serviceMeta: any; 35 | 36 | private calls: { [id: number]: Call } = {}; 37 | private callIdCounter: number = 1; 38 | private serverReleased: boolean = false; 39 | 40 | constructor(private protoTree: any, 41 | private clientId: number, 42 | private info: IGBServiceInfo, 43 | private promise: IServicePromise, 44 | private send: (message: IGBClientMessage) => void) { 45 | } 46 | 47 | public initStub() { 48 | this.serviceMeta = this.protoTree.lookup(this.info.service_id); 49 | this.handle = { 50 | end: () => { 51 | return this.end(); 52 | }, 53 | }; 54 | if (!this.serviceMeta) { 55 | throw new Error('Cannot find identifier ' + this.info.service_id + '.'); 56 | } 57 | if (this.serviceMeta.className !== 'Service') { 58 | throw new Error('Identifier ' 59 | + this.info.service_id 60 | + ' is a ' 61 | + this.serviceMeta.className 62 | + ' not a Service.'); 63 | } 64 | for (let method of this.serviceMeta.children) { 65 | if (method.className !== 'Service.RPCMethod') { 66 | continue; 67 | } 68 | this.buildStubMethod(method); 69 | } 70 | } 71 | 72 | public handleCreateResponse(msg: IGBCreateServiceResult) { 73 | /* istanbul ignore next */ 74 | if (!this.promise) { 75 | return; 76 | } 77 | 78 | if (msg.result === 0) { 79 | this.promise.resolve(this.handle); 80 | return; 81 | } 82 | 83 | if (msg.error_details) { 84 | this.promise.reject(msg.error_details); 85 | } else { 86 | this.promise.reject('Error ' + msg.result); 87 | } 88 | 89 | this.disposed.next(this); 90 | } 91 | 92 | public handleCallCreateResponse(msg: IGBCreateCallResult) { 93 | let call = this.calls[msg.call_id]; 94 | /* istanbul ignore next */ 95 | if (!call) { 96 | return; 97 | } 98 | call.handleCreateResponse(msg); 99 | } 100 | 101 | public handleCallEnded(msg: IGBCallEnded) { 102 | let call = this.calls[msg.call_id]; 103 | /* istanbul ignore next */ 104 | if (!call) { 105 | return; 106 | } 107 | call.handleEnded(msg); 108 | } 109 | 110 | public handleCallEvent(msg: IGBCallEvent) { 111 | let call = this.calls[msg.call_id]; 112 | /* istanbul ignore next */ 113 | if (!call) { 114 | return; 115 | } 116 | call.handleEvent(msg); 117 | } 118 | 119 | public handleServiceRelease(msg: IGBReleaseServiceResult) { 120 | this.serverReleased = true; 121 | this.end(); 122 | } 123 | 124 | public end() { 125 | this.dispose(); 126 | return this; 127 | } 128 | 129 | public dispose() { 130 | for (let callId in this.calls) { 131 | /* istanbul ignore next */ 132 | if (!this.calls.hasOwnProperty(callId)) { 133 | continue; 134 | } 135 | this.calls[callId].terminate(); 136 | } 137 | this.calls = {}; 138 | if (!this.serverReleased) { 139 | this.send({ 140 | service_release: { 141 | service_id: this.clientId, 142 | }, 143 | }); 144 | } 145 | this.disposed.next(this); 146 | } 147 | 148 | private buildStubMethod(methodMeta: any) { 149 | // lowercase first letter 150 | let methodName = methodMeta.name.charAt(0).toLowerCase() + methodMeta.name.slice(1); 151 | this.handle[methodName] = (argument?: any, 152 | callback?: (error?: any, response?: any) => void) => { 153 | if (typeof argument === 'function' && !callback) { 154 | callback = argument; 155 | argument = undefined; 156 | } 157 | return this.startCall(methodMeta, argument, callback); 158 | }; 159 | } 160 | 161 | private startCall(methodMeta: any, 162 | argument?: any, 163 | callback?: (error?: any, response?: any) => void): ICallHandle { 164 | let callId = this.callIdCounter++; 165 | let args: any; 166 | let requestBuilder = methodMeta.resolvedRequestType.build(); 167 | if (argument) { 168 | args = requestBuilder.encode(argument); 169 | } 170 | let info: IGBCallInfo = { 171 | method_id: methodMeta.name, 172 | bin_argument: args, 173 | }; 174 | if (methodMeta.requestStream && argument) { 175 | throw new Error('Argument should not be specified for a request stream.'); 176 | } 177 | if (!methodMeta.requestStream && !argument) { 178 | throw new Error('Argument must be specified for a non-streaming request.'); 179 | } 180 | if (methodMeta.responseStream && callback) { 181 | throw new Error('Callback should not be specified for a response stream.'); 182 | } 183 | if (!methodMeta.responseStream && !callback) { 184 | throw new Error('Callback should be specified for a non-streaming response.'); 185 | } 186 | let call = new Call(callId, this.clientId, info, methodMeta, callback, this.send); 187 | this.calls[callId] = call; 188 | call.disposed.subscribe(() => { 189 | delete this.calls[callId]; 190 | }); 191 | this.send({ 192 | call_create: { 193 | call_id: callId, 194 | info: info, 195 | service_id: this.clientId, 196 | }, 197 | }); 198 | return call; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/client/client.spec.ts: -------------------------------------------------------------------------------- 1 | import { Client } from './client'; 2 | import { IServiceHandle } from './service'; 3 | import { 4 | IGBClientMessage, 5 | } from '../proto'; 6 | import { 7 | buildTree, 8 | } from '../mock'; 9 | 10 | describe('Client', () => { 11 | let client: Client; 12 | let recvQueue: IGBClientMessage[] = []; 13 | let lookupTree: any; 14 | let serviceTree: any; 15 | // An encoded sample request 16 | let encodedData: any; 17 | // An encoded sample response 18 | let encodedResponseData: any; 19 | 20 | beforeEach(() => { 21 | lookupTree = buildTree(); 22 | client = new Client(lookupTree, (msg: IGBClientMessage) => { 23 | recvQueue.push(msg); 24 | }); 25 | serviceTree = client.buildTree(); 26 | encodedData = serviceTree.mock.HelloRequest.encode({'name': 'hello'}).toBase64(); 27 | encodedResponseData = serviceTree.mock.HelloReply.encode({message: 'hello'}).toBase64(); 28 | recvQueue.length = 0; 29 | }); 30 | 31 | it('should build the tree correctly', () => { 32 | let tree = client.buildTree(); 33 | expect(typeof tree).toBe('object'); 34 | expect(typeof tree['mock']).toBe('object'); 35 | expect(typeof tree['mock']['Greeter']).toBe('function'); 36 | 37 | let greeter = tree['mock']['Greeter']('localhost:3000'); 38 | expect(typeof greeter).toBe('object'); 39 | expect(greeter.constructor).toBe(Promise); 40 | expect(recvQueue.length).toBe(1); 41 | expect(recvQueue[0]).toEqual({ 42 | service_create: { 43 | service_id: 1, 44 | service_info: { 45 | service_id: 'mock.Greeter', 46 | endpoint: 'localhost:3000', 47 | }, 48 | }, 49 | }); 50 | }); 51 | 52 | it('should handle service creation errors', (done) => { 53 | let servicePromise: Promise = serviceTree.mock.Greeter('localhost:3000'); 54 | servicePromise.then(() => { 55 | throw new Error('Did not reject promise.'); 56 | }, (err) => { 57 | expect(err).toBe('Error here'); 58 | done(); 59 | }); 60 | client.handleMessage({ 61 | service_create: { 62 | result: 2, 63 | service_id: 1, 64 | error_details: 'Error here', 65 | }, 66 | }); 67 | }); 68 | 69 | it('should handle service creation errors without details', (done) => { 70 | let servicePromise: Promise = serviceTree.mock.Greeter('localhost:3000'); 71 | servicePromise.then(() => { 72 | throw new Error('Did not reject promise.'); 73 | }, (err) => { 74 | expect(err).toBe('Error 2'); 75 | done(); 76 | }); 77 | client.handleMessage({ 78 | service_create: { 79 | result: 2, 80 | service_id: 1, 81 | }, 82 | }); 83 | }); 84 | 85 | it('should not allow invalid callback/argument combos', (done) => { 86 | let servicePromise: Promise = serviceTree.mock.Greeter('localhost:3000'); 87 | client.handleMessage({ 88 | service_create: { 89 | result: 0, 90 | service_id: 1, 91 | }, 92 | }); 93 | servicePromise.then((service) => { 94 | recvQueue.length = 0; 95 | expect(() => { 96 | service['sayHelloClientStream']({}, () => { 97 | // 98 | }); 99 | }).toThrow(new Error('Argument should not be specified for a request stream.')); 100 | expect(() => { 101 | service['sayHello'](() => { 102 | // 103 | }); 104 | }).toThrow(new Error('Argument must be specified for a non-streaming request.')); 105 | expect(() => { 106 | service['sayHello']({}); 107 | }).toThrow(new Error('Callback should be specified for a non-streaming response.')); 108 | expect(() => { 109 | service['sayHelloServerStream']({}, () => { 110 | // 111 | }); 112 | }).toThrow(new Error('Callback should not be specified for a response stream.')); 113 | expect(service['sayHelloServerStream']({})).not.toBe(null); 114 | service.end(); 115 | done(); 116 | }, done); 117 | }); 118 | 119 | it('should start a streaming rpc call correctly', (done) => { 120 | let servicePromise: Promise = serviceTree.mock.Greeter('localhost:3000'); 121 | client.handleMessage({ 122 | service_create: { 123 | result: 0, 124 | service_id: 1, 125 | }, 126 | }); 127 | servicePromise.then((mockService) => { 128 | expect(mockService).not.toBe(null); 129 | recvQueue.length = 0; 130 | let call = mockService['sayHelloBidiStream'](); 131 | expect(recvQueue.length).toBe(1); 132 | let msg = recvQueue[0]; 133 | let throwOnData = false; 134 | recvQueue.length = 0; 135 | call.on('data', (data: any) => { 136 | expect(throwOnData).toBe(false); 137 | expect(data.toRaw()).toEqual({message: 'hello'}); 138 | }); 139 | expect(msg).toEqual({ 140 | call_create: { 141 | call_id: 1, 142 | info: { 143 | method_id: 'SayHelloBidiStream', 144 | bin_argument: undefined, 145 | }, 146 | service_id: 1, 147 | }, 148 | }); 149 | client.handleMessage({ 150 | call_create: { 151 | call_id: 1, 152 | result: 0, 153 | service_id: 1, 154 | }, 155 | }); 156 | // encode test data 157 | client.handleMessage({ 158 | call_event: { 159 | call_id: 1, 160 | service_id: 1, 161 | event: 'data', 162 | bin_data: encodedResponseData, 163 | }, 164 | }); 165 | call.off('data'); 166 | throwOnData = true; 167 | client.handleMessage({ 168 | call_event: { 169 | call_id: 1, 170 | service_id: 1, 171 | event: 'data', 172 | bin_data: encodedResponseData, 173 | }, 174 | }); 175 | call.on('end', () => { 176 | done(); 177 | }); 178 | client.handleMessage({ 179 | call_ended: { 180 | call_id: 1, 181 | service_id: 1, 182 | }, 183 | }); 184 | }); 185 | }); 186 | 187 | it('should start a rpc call correctly', (done) => { 188 | let servicePromise: Promise = serviceTree.mock.Greeter('localhost:3000'); 189 | let serviceCreateMsg = recvQueue[0]; 190 | expect(serviceCreateMsg.service_create.service_id).toBe(1); 191 | client.handleMessage({ 192 | service_create: { 193 | result: 0, 194 | service_id: serviceCreateMsg.service_create.service_id, 195 | }, 196 | }); 197 | recvQueue.length = 0; 198 | let resultAsserted = false; 199 | servicePromise.then((mockService) => { 200 | expect(mockService).not.toBe(null); 201 | mockService['sayHello']({name: 'test'}, (err: any, response: any) => { 202 | expect(response.toRaw()).toEqual({message: 'hello'}); 203 | resultAsserted = true; 204 | }); 205 | expect(recvQueue.length).toBe(1); 206 | let msg = recvQueue[0]; 207 | recvQueue.length = 0; 208 | msg.call_create.info.bin_argument = !!msg.call_create.info.bin_argument; 209 | expect(msg).toEqual({ 210 | call_create: { 211 | call_id: 1, 212 | info: { 213 | method_id: 'SayHello', 214 | bin_argument: true, 215 | }, 216 | service_id: 1, 217 | }, 218 | }); 219 | client.handleMessage({ 220 | call_create: { 221 | call_id: 1, 222 | result: 0, 223 | service_id: 1, 224 | }, 225 | }); 226 | client.handleMessage({ 227 | call_event: { 228 | call_id: 1, 229 | service_id: 1, 230 | event: 'data', 231 | bin_data: encodedData, 232 | }, 233 | }); 234 | client.handleMessage({ 235 | call_ended: { 236 | call_id: 1, 237 | service_id: 1, 238 | }, 239 | }); 240 | expect(resultAsserted).toBe(true); 241 | done(); 242 | }); 243 | }); 244 | 245 | it('should cancel all calls when calling reset', (done) => { 246 | let servicePromise: Promise = serviceTree.mock.Greeter('localhost:3000'); 247 | client.handleMessage({ 248 | service_create: { 249 | result: 0, 250 | service_id: 1, 251 | }, 252 | }); 253 | servicePromise.then((mockService) => { 254 | expect(mockService).not.toBe(null); 255 | mockService['sayHelloBidiStream'](); 256 | client.handleMessage({ 257 | call_create: { 258 | call_id: 1, 259 | result: 0, 260 | service_id: 1, 261 | }, 262 | }); 263 | client.handleMessage({ 264 | call_event: { 265 | call_id: 1, 266 | service_id: 1, 267 | event: 'data', 268 | bin_data: encodedData, 269 | }, 270 | }); 271 | recvQueue.length = 0; 272 | client.reset(); 273 | expect(recvQueue).toEqual([{ 274 | call_end: { 275 | call_id: 1, 276 | service_id: 1, 277 | }, 278 | }, { 279 | service_release: { 280 | service_id: 1, 281 | }, 282 | }]); 283 | done(); 284 | }); 285 | }); 286 | 287 | it('should handle a create error properly', (done) => { 288 | let servicePromise: Promise = serviceTree.mock.Greeter('localhost:3000'); 289 | client.handleMessage({ 290 | service_create: { 291 | result: 0, 292 | service_id: 1, 293 | }, 294 | }); 295 | recvQueue.length = 0; 296 | servicePromise.then((mockService) => { 297 | expect(mockService).not.toBe(null); 298 | mockService['sayHello']({name: 'test'}, (err: any, response: any) => { 299 | expect(response).toBe(null); 300 | expect(err).toBe('Error on server side.'); 301 | done(); 302 | }); 303 | client.handleMessage({ 304 | call_create: { 305 | call_id: 1, 306 | service_id: 1, 307 | result: 2, 308 | error_details: 'Error on server side.', 309 | }, 310 | }); 311 | }); 312 | }); 313 | }); 314 | -------------------------------------------------------------------------------- /src/proto/definitions.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:trailing-comma */ 2 | /* tslint:disable:quotemark */ 3 | /* tslint:disable:max-line-length */ 4 | export const PROTO_DEFINITIONS = { 5 | "package": "grpcbus", 6 | "syntax": "proto3", 7 | "messages": [ 8 | { 9 | "name": "GBClientMessage", 10 | "fields": [ 11 | { 12 | "rule": "optional", 13 | "type": "GBCreateService", 14 | "name": "service_create", 15 | "id": 1 16 | }, 17 | { 18 | "rule": "optional", 19 | "type": "GBReleaseService", 20 | "name": "service_release", 21 | "id": 2 22 | }, 23 | { 24 | "rule": "optional", 25 | "type": "GBCreateCall", 26 | "name": "call_create", 27 | "id": 3 28 | }, 29 | { 30 | "rule": "optional", 31 | "type": "GBCallEnd", 32 | "name": "call_end", 33 | "id": 4 34 | }, 35 | { 36 | "rule": "optional", 37 | "type": "GBSendCall", 38 | "name": "call_send", 39 | "id": 5 40 | } 41 | ], 42 | "syntax": "proto3" 43 | }, 44 | { 45 | "name": "GBServerMessage", 46 | "fields": [ 47 | { 48 | "rule": "optional", 49 | "type": "GBCreateServiceResult", 50 | "name": "service_create", 51 | "id": 1 52 | }, 53 | { 54 | "rule": "optional", 55 | "type": "GBReleaseServiceResult", 56 | "name": "service_release", 57 | "id": 2 58 | }, 59 | { 60 | "rule": "optional", 61 | "type": "GBCreateCallResult", 62 | "name": "call_create", 63 | "id": 3 64 | }, 65 | { 66 | "rule": "optional", 67 | "type": "GBCallEvent", 68 | "name": "call_event", 69 | "id": 4 70 | }, 71 | { 72 | "rule": "optional", 73 | "type": "GBCallEnded", 74 | "name": "call_ended", 75 | "id": 5 76 | } 77 | ], 78 | "syntax": "proto3" 79 | }, 80 | { 81 | "name": "GBServiceInfo", 82 | "fields": [ 83 | { 84 | "rule": "optional", 85 | "type": "string", 86 | "name": "endpoint", 87 | "id": 1 88 | }, 89 | { 90 | "rule": "optional", 91 | "type": "string", 92 | "name": "service_id", 93 | "id": 2 94 | } 95 | ], 96 | "syntax": "proto3" 97 | }, 98 | { 99 | "name": "GBCreateService", 100 | "fields": [ 101 | { 102 | "rule": "optional", 103 | "type": "int32", 104 | "name": "service_id", 105 | "id": 1 106 | }, 107 | { 108 | "rule": "optional", 109 | "type": "GBServiceInfo", 110 | "name": "service_info", 111 | "id": 2 112 | } 113 | ], 114 | "syntax": "proto3" 115 | }, 116 | { 117 | "name": "GBReleaseService", 118 | "fields": [ 119 | { 120 | "rule": "optional", 121 | "type": "int32", 122 | "name": "service_id", 123 | "id": 1 124 | } 125 | ], 126 | "syntax": "proto3" 127 | }, 128 | { 129 | "name": "GBCallInfo", 130 | "fields": [ 131 | { 132 | "rule": "optional", 133 | "type": "string", 134 | "name": "method_id", 135 | "id": 1 136 | }, 137 | { 138 | "rule": "optional", 139 | "type": "bytes", 140 | "name": "bin_argument", 141 | "id": 2 142 | } 143 | ], 144 | "syntax": "proto3" 145 | }, 146 | { 147 | "name": "GBCreateCall", 148 | "fields": [ 149 | { 150 | "rule": "optional", 151 | "type": "int32", 152 | "name": "service_id", 153 | "id": 1 154 | }, 155 | { 156 | "rule": "optional", 157 | "type": "int32", 158 | "name": "call_id", 159 | "id": 2 160 | }, 161 | { 162 | "rule": "optional", 163 | "type": "GBCallInfo", 164 | "name": "info", 165 | "id": 3 166 | } 167 | ], 168 | "syntax": "proto3" 169 | }, 170 | { 171 | "name": "GBCallEnded", 172 | "fields": [ 173 | { 174 | "rule": "optional", 175 | "type": "int32", 176 | "name": "call_id", 177 | "id": 1 178 | }, 179 | { 180 | "rule": "optional", 181 | "type": "int32", 182 | "name": "service_id", 183 | "id": 2 184 | } 185 | ], 186 | "syntax": "proto3" 187 | }, 188 | { 189 | "name": "GBEndCall", 190 | "fields": [ 191 | { 192 | "rule": "optional", 193 | "type": "int32", 194 | "name": "call_id", 195 | "id": 1 196 | }, 197 | { 198 | "rule": "optional", 199 | "type": "int32", 200 | "name": "service_id", 201 | "id": 2 202 | } 203 | ], 204 | "syntax": "proto3" 205 | }, 206 | { 207 | "name": "GBSendCall", 208 | "fields": [ 209 | { 210 | "rule": "optional", 211 | "type": "int32", 212 | "name": "call_id", 213 | "id": 1 214 | }, 215 | { 216 | "rule": "optional", 217 | "type": "int32", 218 | "name": "service_id", 219 | "id": 2 220 | }, 221 | { 222 | "rule": "optional", 223 | "type": "bytes", 224 | "name": "bin_data", 225 | "id": 3 226 | }, 227 | { 228 | "rule": "optional", 229 | "type": "bool", 230 | "name": "is_end", 231 | "id": 4 232 | } 233 | ], 234 | "syntax": "proto3" 235 | }, 236 | { 237 | "name": "GBCreateServiceResult", 238 | "fields": [ 239 | { 240 | "rule": "optional", 241 | "type": "int32", 242 | "name": "service_id", 243 | "id": 1 244 | }, 245 | { 246 | "rule": "optional", 247 | "type": "ECreateServiceResult", 248 | "name": "result", 249 | "id": 2 250 | }, 251 | { 252 | "rule": "optional", 253 | "type": "string", 254 | "name": "error_details", 255 | "id": 3 256 | } 257 | ], 258 | "syntax": "proto3", 259 | "enums": [ 260 | { 261 | "name": "ECreateServiceResult", 262 | "values": [ 263 | { 264 | "name": "SUCCESS", 265 | "id": 0 266 | }, 267 | { 268 | "name": "INVALID_ID", 269 | "id": 1 270 | }, 271 | { 272 | "name": "GRPC_ERROR", 273 | "id": 2 274 | } 275 | ], 276 | "syntax": "proto3" 277 | } 278 | ] 279 | }, 280 | { 281 | "name": "GBReleaseServiceResult", 282 | "fields": [ 283 | { 284 | "rule": "optional", 285 | "type": "int32", 286 | "name": "service_id", 287 | "id": 1 288 | } 289 | ], 290 | "syntax": "proto3" 291 | }, 292 | { 293 | "name": "GBCreateCallResult", 294 | "fields": [ 295 | { 296 | "rule": "optional", 297 | "type": "int32", 298 | "name": "call_id", 299 | "id": 1 300 | }, 301 | { 302 | "rule": "optional", 303 | "type": "int32", 304 | "name": "service_id", 305 | "id": 4 306 | }, 307 | { 308 | "rule": "optional", 309 | "type": "ECreateCallResult", 310 | "name": "result", 311 | "id": 2 312 | }, 313 | { 314 | "rule": "optional", 315 | "type": "string", 316 | "name": "error_details", 317 | "id": 3 318 | } 319 | ], 320 | "syntax": "proto3", 321 | "enums": [ 322 | { 323 | "name": "ECreateCallResult", 324 | "values": [ 325 | { 326 | "name": "SUCCESS", 327 | "id": 0 328 | }, 329 | { 330 | "name": "INVALID_ID", 331 | "id": 1 332 | }, 333 | { 334 | "name": "GRPC_ERROR", 335 | "id": 2 336 | } 337 | ], 338 | "syntax": "proto3" 339 | } 340 | ] 341 | }, 342 | { 343 | "name": "GBCallEvent", 344 | "fields": [ 345 | { 346 | "rule": "optional", 347 | "type": "int32", 348 | "name": "call_id", 349 | "id": 1 350 | }, 351 | { 352 | "rule": "optional", 353 | "type": "int32", 354 | "name": "service_id", 355 | "id": 4 356 | }, 357 | { 358 | "rule": "optional", 359 | "type": "string", 360 | "name": "event", 361 | "id": 2 362 | }, 363 | { 364 | "rule": "optional", 365 | "type": "string", 366 | "name": "json_data", 367 | "id": 3 368 | }, 369 | { 370 | "rule": "optional", 371 | "type": "bytes", 372 | "name": "bin_data", 373 | "id": 5 374 | } 375 | ], 376 | "syntax": "proto3" 377 | }, 378 | { 379 | "name": "GBCallEnd", 380 | "fields": [ 381 | { 382 | "rule": "optional", 383 | "type": "int32", 384 | "name": "call_id", 385 | "id": 1 386 | }, 387 | { 388 | "rule": "optional", 389 | "type": "int32", 390 | "name": "service_id", 391 | "id": 2 392 | } 393 | ], 394 | "syntax": "proto3" 395 | } 396 | ], 397 | "isNamespace": true 398 | }; 399 | --------------------------------------------------------------------------------