├── .eslintignore ├── src ├── utils │ ├── index.ts │ ├── error.ts │ └── logger.ts ├── models │ ├── .gitignore │ └── tsconfig.json ├── services │ ├── Health.ts │ └── Greeter.ts ├── client.service.ts ├── server.ts ├── health.ts └── client.ts ├── .gitattributes ├── .prettierrc ├── tsconfig.json ├── LICENSE ├── proto ├── helloworld.proto └── health.proto ├── .eslintrc ├── bin └── proto.js ├── README.md ├── .gitignore └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | dist/** 3 | src/models/** 4 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error'; 2 | export * from './logger'; 3 | -------------------------------------------------------------------------------- /src/models/.gitignore: -------------------------------------------------------------------------------- 1 | # protoc output folder 2 | * 3 | !.gitignore 4 | !tsconfig.json 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # JS and TS files must always use LF for tools to work 5 | *.js eol=lf 6 | *.ts eol=lf 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /src/models/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "../../dist/models", 6 | "noImplicitReturns": false 7 | }, 8 | "include": ["**/*"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { Metadata, ServiceError as grpcServiceError, status } from '@grpc/grpc-js'; 2 | 3 | /** 4 | * https://grpc.io/grpc/node/grpc.html#~ServiceError__anchor 5 | */ 6 | export class ServiceError extends Error implements Partial { 7 | public override name: string = 'ServiceError'; 8 | 9 | constructor( 10 | public code: status, 11 | public override message: string, 12 | public details?: string, 13 | public metadata?: Metadata 14 | ) { 15 | super(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/Health.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from '@grpc/grpc-js'; 2 | import { HealthImplementation, ServingStatus, ServingStatusMap } from 'grpc-health-check'; 3 | 4 | export class Health { 5 | private servingStatus: ServingStatusMap = { 6 | '': 'NOT_SERVING', 7 | 'helloworld.Greeter': 'NOT_SERVING', 8 | }; 9 | 10 | private healthImpl = new HealthImplementation(this.servingStatus); 11 | 12 | constructor(server: Server) { 13 | this.healthImpl.addToServer(server); 14 | } 15 | 16 | public setStatus(service: string, status: ServingStatus): void { 17 | this.healthImpl.setStatus(service, status); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client.service.ts: -------------------------------------------------------------------------------- 1 | import { credentials, Metadata } from '@grpc/grpc-js'; 2 | import { promisify } from 'util'; 3 | 4 | import { GreeterClient, HelloRequest, HelloResponse } from './models/helloworld'; 5 | 6 | /** 7 | * gRPC GreeterClient Service 8 | * https://github.com/grpc/grpc-node/issues/54 9 | */ 10 | class ClientService { 11 | private readonly client: GreeterClient = new GreeterClient('localhost:50051', credentials.createInsecure()); 12 | 13 | public async sayHello(param: HelloRequest, metadata: Metadata = new Metadata()): Promise { 14 | return promisify(this.client.sayHello.bind(this.client))(param, metadata); 15 | } 16 | } 17 | 18 | export const clientService: ClientService = new ClientService(); 19 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import { Server, ServerCredentials } from '@grpc/grpc-js'; 3 | 4 | import { Greeter, GreeterService } from './services/Greeter'; 5 | import { Health } from './services/Health'; 6 | import { logger } from './utils'; 7 | 8 | // Do not use @grpc/proto-loader 9 | const server = new Server({ 10 | 'grpc.max_receive_message_length': -1, 11 | 'grpc.max_send_message_length': -1, 12 | }); 13 | 14 | server.addService(GreeterService, new Greeter()); 15 | const health = new Health(server); 16 | 17 | server.bindAsync('0.0.0.0:50051', ServerCredentials.createInsecure(), (err: Error | null, bindPort: number) => { 18 | if (err) { 19 | throw err; 20 | } 21 | 22 | logger.info(`gRPC:Server:${bindPort}`, new Date().toLocaleString()); 23 | 24 | // Change service health status 25 | health.setStatus('helloworld.Greeter', 'SERVING'); 26 | }); 27 | 28 | export { server, health }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": {}, 5 | "target": "ES2021", 6 | "outDir": "dist", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "incremental": true, 10 | "declaration": true, 11 | "newLine": "lf", 12 | "strict": true, 13 | "allowUnreachableCode": false, 14 | "allowUnusedLabels": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitOverride": true, 17 | "noImplicitReturns": true, 18 | "noPropertyAccessFromIndexSignature": true, 19 | // "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "removeComments": true, 23 | "sourceMap": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "esModuleInterop": true, 26 | "skipLibCheck": true 27 | }, 28 | "include": ["src/**/*"], 29 | "exclude": ["node_modules", "src/models"], 30 | "references": [ 31 | { "path": "src/models" } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/health.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import { credentials, ServiceError } from '@grpc/grpc-js'; 3 | 4 | import { HealthClient, HealthCheckRequest, HealthCheckResponse, HealthCheckResponse_ServingStatus } from './models/health'; 5 | import { logger } from './utils'; 6 | 7 | const health = new HealthClient('localhost:50051', credentials.createInsecure()); 8 | logger.info('gRPC:HealthClient', new Date().toLocaleString()); 9 | 10 | let argv = 'helloworld.Greeter'; 11 | if (process.argv.length >= 3) { 12 | [, , argv] = process.argv; 13 | } 14 | 15 | const param: HealthCheckRequest = { 16 | service: argv, 17 | }; 18 | 19 | health.check(param, (err: ServiceError | null, res: HealthCheckResponse) => { 20 | if (err) { 21 | logger.error('healthCheck:', err); 22 | return; 23 | } 24 | 25 | const { status } = res; 26 | if (status !== HealthCheckResponse_ServingStatus.SERVING) { 27 | logger.error('healthCheck:', status); 28 | return; 29 | } 30 | 31 | logger.info('healthCheck:', status); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CatsMiaow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /proto/helloworld.proto: -------------------------------------------------------------------------------- 1 | // https://developers.google.com/protocol-buffers/docs/proto3?hl=ko#json 2 | // https://developers.google.com/protocol-buffers/docs/reference/proto3-spec 3 | syntax = "proto3"; 4 | 5 | // https://developers.google.com/protocol-buffers/docs/proto3#packages-and-name-resolution 6 | package helloworld; 7 | 8 | // https://developers.google.com/protocol-buffers/docs/reference/google.protobuf 9 | import "google/protobuf/struct.proto"; 10 | 11 | // https://developers.google.com/protocol-buffers/docs/style 12 | service Greeter { 13 | rpc SayHello (HelloRequest) returns (HelloResponse); 14 | rpc SayHelloStreamRequest (stream HelloRequest) returns (HelloResponse) {} 15 | rpc SayHelloStreamResponse (HelloRequest) returns (stream HelloResponse) {} 16 | rpc SayHelloStream (stream HelloRequest) returns (stream HelloResponse) {} 17 | } 18 | 19 | message HelloRequest { 20 | string name = 1; 21 | 22 | google.protobuf.Struct param_struct = 2; 23 | google.protobuf.ListValue param_list_value = 3; 24 | google.protobuf.Value param_value = 4; 25 | } 26 | 27 | message HelloResponse { 28 | string message = 1; 29 | bool snake_case = 2; 30 | 31 | google.protobuf.Struct param_struct = 3; 32 | google.protobuf.ListValue param_list_value = 4; 33 | google.protobuf.Value param_value = 5; 34 | } 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "tsconfigRootDir": ".", 6 | "project": "tsconfig.json", 7 | "sourceType": "module" 8 | }, 9 | "plugins": ["@typescript-eslint", "sonarjs", "prettier"], 10 | "extends": [ 11 | "eslint:recommended", 12 | "airbnb-base", 13 | "airbnb-typescript/base", 14 | "plugin:sonarjs/recommended", 15 | "plugin:@typescript-eslint/strict-type-checked", 16 | "plugin:@typescript-eslint/stylistic-type-checked", 17 | "plugin:prettier/recommended" 18 | ], 19 | "rules": { 20 | "class-methods-use-this": "off", 21 | "consistent-return": "off", 22 | "max-len": "off", 23 | "no-restricted-syntax": "off", 24 | "object-curly-newline": "off", 25 | 26 | "import/prefer-default-export": "off", 27 | "import/order": ["error", { "groups": [["builtin", "external", "internal"]], "newlines-between": "always", "alphabetize": { "order": "asc", "caseInsensitive": true } }], 28 | 29 | "@typescript-eslint/consistent-type-assertions": ["error", { "assertionStyle": "angle-bracket" }], 30 | "@typescript-eslint/no-floating-promises": "off", 31 | "@typescript-eslint/no-inferrable-types": "off", 32 | "@typescript-eslint/restrict-template-expressions": "off", 33 | 34 | "sonarjs/no-duplicate-string": "off" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bin/proto.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { execSync } = require('child_process'); 3 | const { rimrafSync } = require('rimraf'); 4 | const { protoPath: healthProtoPath } = require('grpc-health-check'); 5 | const { copyFileSync } = require('fs'); 6 | 7 | const PROTO_DIR = join(__dirname, '../proto'); 8 | const MODEL_DIR = join(__dirname, '../src/models'); 9 | const PROTOC_PATH = join(__dirname, "../node_modules/grpc-tools/bin/protoc"); 10 | const PLUGIN_PATH = join(__dirname, "../node_modules/.bin/protoc-gen-ts_proto"); 11 | 12 | rimrafSync(`${MODEL_DIR}/*`, { 13 | glob: { ignore: `${MODEL_DIR}/tsconfig.json` }, 14 | }); 15 | 16 | copyFileSync(healthProtoPath, join(PROTO_DIR, 'health.proto')); 17 | 18 | // https://github.com/stephenh/ts-proto/blob/main/README.markdown#supported-options 19 | const tsProtoOpt = [ 20 | 'outputServices=grpc-js', 21 | 'env=node', 22 | 'useOptionals=messages', 23 | 'exportCommonSymbols=false', 24 | 'esModuleInterop=true', 25 | ]; 26 | 27 | const protoConfig = [ 28 | `--plugin=${PLUGIN_PATH}`, 29 | `--ts_proto_opt=${tsProtoOpt.join(',')}`, 30 | `--ts_proto_out=${MODEL_DIR}`, 31 | `--proto_path ${PROTO_DIR} ${PROTO_DIR}/*.proto`, 32 | ]; 33 | 34 | // https://github.com/stephenh/ts-proto#usage 35 | execSync(`${PROTOC_PATH} ${protoConfig.join(" ")}`); 36 | console.log(`> Proto models created: ${MODEL_DIR}`); 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-grpc-typescript 2 | 3 | Node.js gRPC structure with [google.protobuf](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf) for TypeScript example 4 | 5 | - This example uses [ts-proto](https://github.com/stephenh/ts-proto) as the TypeScript plugin. 6 | - For an example using the [grpc_tools_node_protoc_ts](https://github.com/agreatfool/grpc_tools_node_protoc_ts) plugin, see the following [branch](https://github.com/CatsMiaow/node-grpc-typescript/tree/grpc_tools_node_protoc_ts) source. 7 | 8 | ## Installation 9 | 10 | ```sh 11 | npm ci 12 | ``` 13 | 14 | ## Build 15 | 16 | ```sh 17 | # exports to *.proto to *.ts 18 | npm run build 19 | npm run lint 20 | ``` 21 | 22 | ## Server Start 23 | 24 | ```sh 25 | npm start #= node dist/server 26 | ``` 27 | 28 | ## Client Test 29 | 30 | Start the server before testing client commands. 31 | 32 | ```sh 33 | # 1. Request 34 | npm run client #= node dist/client 35 | # 2. with Parameter 36 | npm run client blahblahblah 37 | # 3. Error 38 | npm run client error 39 | # 4. Stream 40 | npm run client stream 41 | # 5. Health Check 42 | npm run health 43 | ``` 44 | 45 | ### Documentation 46 | 47 | - [Node.js gRPC Documentation](https://grpc.io/grpc/node/grpc.html) 48 | - [Protocol Buffers](https://developers.google.com/protocol-buffers/docs/proto3) 49 | - [gRPC for Node.js](https://github.com/grpc/grpc-node) 50 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-optional-chain, @typescript-eslint/non-nullable-type-assertion-style */ 2 | import { dirname } from 'path'; 3 | 4 | export type Parameter = [unknown?, ...unknown[]]; 5 | 6 | export class Logger { 7 | private readonly rootDir: string = dirname((require.main).filename); 8 | 9 | constructor() { 10 | this.rootDir = this.rootDir.replace('/dist', '/src'); 11 | } 12 | 13 | public info(...args: Parameter): void { 14 | args.push(`- ${this.trace()}`); 15 | console.info(...args); 16 | } 17 | 18 | public warn(...args: Parameter): void { 19 | args.push(`- ${this.trace()}`); 20 | console.warn(...args); 21 | } 22 | 23 | public error(...args: Parameter): void { 24 | args.push(`- ${this.trace()}`); 25 | console.error(...args); 26 | } 27 | 28 | private trace(): string { 29 | const lines = (new Error().stack).split('\n').slice(1); 30 | const lineMatch = /at (?:(.+)\s+)?\(?(?:(.+?):(\d+):(\d+)|([^)]+))\)?/.exec(lines[2]); 31 | 32 | if (!lineMatch || lineMatch[2] === null || lineMatch[3] === null) { 33 | return ''; 34 | } 35 | 36 | const fileName = lineMatch[2].split(this.rootDir)[1]; 37 | const line = lineMatch[3]; 38 | 39 | return `${fileName}:${line}`; 40 | } 41 | } 42 | 43 | export const logger: Logger = new Logger(); 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .history 4 | .DS_Store 5 | /dist/ 6 | 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-grpc-typescript", 3 | "version": "0.1.0", 4 | "description": "Node.js gRPC Structure for TypeScript Example", 5 | "main": "dist/server.js", 6 | "engines": { 7 | "node": ">=22" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "lint": "eslint --ext .ts .", 12 | "build": "node bin/proto && rimraf dist && tsc -b", 13 | "start": "node dist/server", 14 | "client": "node dist/client", 15 | "health": "node dist/health" 16 | }, 17 | "dependencies": { 18 | "@grpc/grpc-js": "^1.13.3", 19 | "grpc-health-check": "^2.0.2", 20 | "source-map-support": "^0.5.21" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^22.15.3", 24 | "@typescript-eslint/eslint-plugin": "^7.18.0", 25 | "@typescript-eslint/parser": "^7.18.0", 26 | "eslint": "^8.57.1", 27 | "eslint-config-airbnb-base": "^15.0.0", 28 | "eslint-config-airbnb-typescript": "^18.0.0", 29 | "eslint-config-prettier": "^9.1.0", 30 | "eslint-plugin-import": "^2.31.0", 31 | "eslint-plugin-prettier": "^5.2.6", 32 | "eslint-plugin-sonarjs": "^0.25.1", 33 | "grpc-tools": "^1.13.0", 34 | "rimraf": "^6.0.1", 35 | "ts-proto": "^2.7.0", 36 | "typescript": "~5.8.3" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/CatsMiaow/node-grpc-typescript.git" 41 | }, 42 | "keywords": [ 43 | "Node.js", 44 | "gRPC", 45 | "TypeScript" 46 | ], 47 | "homepage": "https://github.com/CatsMiaow/node-grpc-typescript#readme", 48 | "author": "CatsMiaow", 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /proto/health.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The gRPC Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The canonical version of this proto can be found at 16 | // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto 17 | 18 | syntax = "proto3"; 19 | 20 | package grpc.health.v1; 21 | 22 | option csharp_namespace = "Grpc.Health.V1"; 23 | option go_package = "google.golang.org/grpc/health/grpc_health_v1"; 24 | option java_multiple_files = true; 25 | option java_outer_classname = "HealthProto"; 26 | option java_package = "io.grpc.health.v1"; 27 | 28 | message HealthCheckRequest { 29 | string service = 1; 30 | } 31 | 32 | message HealthCheckResponse { 33 | enum ServingStatus { 34 | UNKNOWN = 0; 35 | SERVING = 1; 36 | NOT_SERVING = 2; 37 | SERVICE_UNKNOWN = 3; // Used only by the Watch method. 38 | } 39 | ServingStatus status = 1; 40 | } 41 | 42 | // Health is gRPC's mechanism for checking whether a server is able to handle 43 | // RPCs. Its semantics are documented in 44 | // https://github.com/grpc/grpc/blob/master/doc/health-checking.md. 45 | service Health { 46 | // Check gets the health of the specified service. If the requested service 47 | // is unknown, the call will fail with status NOT_FOUND. If the caller does 48 | // not specify a service name, the server should respond with its overall 49 | // health status. 50 | // 51 | // Clients should set a deadline when calling Check, and can declare the 52 | // server unhealthy if they do not receive a timely response. 53 | // 54 | // Check implementations should be idempotent and side effect free. 55 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 56 | 57 | // Performs a watch for the serving status of the requested service. 58 | // The server will immediately send back a message indicating the current 59 | // serving status. It will then subsequently send a new message whenever 60 | // the service's serving status changes. 61 | // 62 | // If the requested service is unknown when the call is received, the 63 | // server will send a message setting the serving status to 64 | // SERVICE_UNKNOWN but will *not* terminate the call. If at some 65 | // future point, the serving status of the service becomes known, the 66 | // server will send a new message with the service's serving status. 67 | // 68 | // If the call terminates with status UNIMPLEMENTED, then clients 69 | // should assume this method is not supported and should not retry the 70 | // call. If the call terminates with any other status (including OK), 71 | // clients should retry the call with appropriate exponential backoff. 72 | rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); 73 | } 74 | -------------------------------------------------------------------------------- /src/services/Greeter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sendUnaryData, 3 | ServerDuplexStream, 4 | ServerReadableStream, 5 | ServerUnaryCall, 6 | ServerWritableStream, 7 | status, 8 | UntypedHandleCall, 9 | } from '@grpc/grpc-js'; 10 | import { randomBytes } from 'crypto'; 11 | 12 | import { GreeterServer, GreeterService, HelloRequest, HelloResponse } from '../models/helloworld'; 13 | import { logger, ServiceError } from '../utils'; 14 | 15 | /** 16 | * package helloworld 17 | * service Greeter 18 | */ 19 | class Greeter implements GreeterServer { 20 | [method: string]: UntypedHandleCall; 21 | 22 | /** 23 | * Implements the SayHello RPC method. 24 | */ 25 | public sayHello(call: ServerUnaryCall, callback: sendUnaryData): void { 26 | logger.info('sayHello', Date.now()); 27 | 28 | const res: Partial = {}; 29 | const { name } = call.request; 30 | logger.info('sayHelloName:', name); 31 | 32 | if (name === 'error') { 33 | // https://grpc.io/grpc/node/grpc.html#.status__anchor 34 | callback(new ServiceError(status.INVALID_ARGUMENT, 'InvalidValue'), null); 35 | return; 36 | } 37 | 38 | const metadataValue = call.metadata.get('foo'); 39 | logger.info('sayHelloMetadata:', metadataValue); 40 | 41 | res.message = metadataValue.length > 0 ? `foo is ${metadataValue}` : `hello ${name}`; 42 | 43 | const { paramStruct, paramListValue } = call.request; 44 | const paramValue = call.request.paramValue; 45 | logger.info('sayHelloStruct:', paramStruct); 46 | logger.info('sayHelloListValue:', paramListValue); 47 | logger.info('sayHelloValue:', paramValue); 48 | 49 | res.paramStruct = paramStruct; 50 | res.paramListValue = paramListValue; 51 | res.paramValue = paramValue; 52 | 53 | callback(null, HelloResponse.fromJSON(res)); 54 | } 55 | 56 | public sayHelloStreamRequest(call: ServerReadableStream, callback: sendUnaryData): void { 57 | logger.info('sayHelloStreamRequest:', call.getPeer()); 58 | 59 | const data: string[] = []; 60 | call 61 | .on('data', (req: HelloRequest) => { 62 | data.push(`${req.name} - ${randomBytes(5).toString('hex')}`); 63 | }) 64 | .on('end', () => { 65 | callback( 66 | null, 67 | HelloResponse.fromJSON({ 68 | message: data.join('\n'), 69 | }) 70 | ); 71 | }) 72 | .on('error', (err: Error) => { 73 | callback(new ServiceError(status.INTERNAL, err.message), null); 74 | }); 75 | } 76 | 77 | public sayHelloStreamResponse(call: ServerWritableStream): void { 78 | logger.info('sayHelloStreamResponse:', call.request); 79 | 80 | const { name } = call.request; 81 | 82 | for (const text of Array(10) 83 | .fill('') 84 | .map(() => randomBytes(5).toString('hex'))) { 85 | call.write( 86 | HelloResponse.fromJSON({ 87 | message: `${name} - ${text}`, 88 | }) 89 | ); 90 | } 91 | call.end(); 92 | } 93 | 94 | public sayHelloStream(call: ServerDuplexStream): void { 95 | logger.info('sayHelloStream:', call.getPeer()); 96 | 97 | call 98 | .on('data', (req: HelloRequest) => { 99 | call.write( 100 | HelloResponse.fromJSON({ 101 | message: `${req.name} - ${randomBytes(5).toString('hex')}`, 102 | }) 103 | ); 104 | }) 105 | .on('end', () => { 106 | call.end(); 107 | }) 108 | .on('error', (err: Error) => { 109 | logger.error('sayHelloStream:', err); 110 | }); 111 | } 112 | } 113 | 114 | export { Greeter, GreeterService }; 115 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import { credentials, Metadata, ServiceError } from '@grpc/grpc-js'; 3 | 4 | import { clientService } from './client.service'; 5 | import { GreeterClient, HelloRequest, HelloResponse } from './models/helloworld'; 6 | import { logger } from './utils'; 7 | 8 | // https://github.com/grpc/grpc/blob/master/doc/keepalive.md 9 | // https://cloud.ibm.com/docs/blockchain-multicloud?topic=blockchain-multicloud-best-practices-app#best-practices-app-connections 10 | const client = new GreeterClient('localhost:50051', credentials.createInsecure(), { 11 | 'grpc.keepalive_time_ms': 120000, 12 | 'grpc.http2.min_time_between_pings_ms': 120000, 13 | 'grpc.keepalive_timeout_ms': 20000, 14 | 'grpc.http2.max_pings_without_data': 0, 15 | 'grpc.keepalive_permit_without_calls': 1, 16 | }); 17 | logger.info('gRPC:GreeterClient', new Date().toLocaleString()); 18 | 19 | let argv = 'world'; 20 | if (process.argv.length >= 3) { 21 | [, , argv] = process.argv; 22 | } 23 | 24 | const param: HelloRequest = { 25 | name: argv, 26 | paramStruct: { foo: 'bar', bar: 'foo' }, 27 | paramListValue: [{ foo: 'bar' }, { bar: 'foo' }], 28 | paramValue: 'Any Value', 29 | }; 30 | 31 | const metadata = new Metadata(); 32 | metadata.add('foo', 'bar1'); 33 | metadata.add('foo', 'bar2'); 34 | 35 | async function example(): Promise { 36 | /** 37 | * rpc sayHello with callback 38 | * https://github.com/grpc/grpc-node/issues/54 39 | */ 40 | client.sayHello(param, (err: ServiceError | null, res: HelloResponse) => { 41 | if (err) { 42 | logger.error('sayBasic:', err.message); 43 | return; 44 | } 45 | 46 | logger.info('sayBasic:', res.message); 47 | }); 48 | 49 | /** 50 | * rpc sayHello with Promise 51 | */ 52 | const sayHello = await clientService.sayHello(param); 53 | logger.info('sayHello:', sayHello.message); 54 | logger.info('sayHelloStruct:', sayHello.paramStruct); 55 | logger.info('sayHelloListValue:', sayHello.paramListValue); 56 | logger.info('sayHelloValue:', sayHello.paramValue); 57 | 58 | /** 59 | * rpc sayHello with Metadata 60 | */ 61 | const sayHelloMetadata = await clientService.sayHello(param, metadata); 62 | logger.info('sayHelloMetadata:', sayHelloMetadata.message); 63 | } 64 | 65 | function exampleStream(): void { 66 | /** 67 | * rpc sayHelloStreamRequest 68 | */ 69 | const streamRequest = client.sayHelloStreamRequest((err: ServiceError | null, res: HelloResponse) => { 70 | if (err) { 71 | logger.error('sayHelloStreamRequest:', err); 72 | return; 73 | } 74 | 75 | logger.info('sayHelloStreamRequest:', res.message); 76 | }); 77 | 78 | for (let i = 1; i <= 10; i += 1) { 79 | streamRequest.write({ 80 | name: `${argv}.${i}`, 81 | }); 82 | } 83 | streamRequest.end(); 84 | 85 | /** 86 | * rpc sayHelloStreamResponse 87 | */ 88 | const streamResponse = client.sayHelloStreamResponse(param); 89 | 90 | const data: string[] = []; 91 | streamResponse 92 | .on('data', (res: HelloResponse) => { 93 | data.push(res.message); 94 | }) 95 | .on('end', () => { 96 | logger.info('sayHelloStreamResponse:', data.join('\n')); 97 | }) 98 | .on('error', (err: Error) => { 99 | logger.error('sayHelloStreamResponse:', err); 100 | }); 101 | 102 | /** 103 | * rpc sayHelloStream 104 | */ 105 | const stream = client.sayHelloStream(); 106 | stream 107 | .on('data', (res: HelloResponse) => { 108 | logger.info('sayHelloStream:', res.message); 109 | }) 110 | .on('end', () => { 111 | logger.info('sayHelloStream: End'); 112 | }) 113 | .on('error', (err: Error) => { 114 | logger.error('sayHelloStream:', err); 115 | }); 116 | 117 | for (let i = 1; i <= 10; i += 1) { 118 | stream.write({ 119 | name: `${argv}.${i}`, 120 | }); 121 | } 122 | stream.end(); 123 | } 124 | 125 | (async (): Promise => { 126 | try { 127 | if (argv === 'stream') { 128 | exampleStream(); 129 | return; 130 | } 131 | 132 | await example(); 133 | } catch (err) { 134 | logger.error(err); 135 | } 136 | })(); 137 | --------------------------------------------------------------------------------