@themainstack/communication 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/grpc/client-factory.d.ts +5 -0
- package/dist/grpc/client-factory.js +73 -0
- package/dist/grpc/errors.d.ts +8 -0
- package/dist/grpc/errors.js +24 -0
- package/dist/grpc/server-factory.d.ts +98 -0
- package/dist/grpc/server-factory.js +220 -0
- package/dist/grpc/types.d.ts +10 -0
- package/dist/grpc/types.js +2 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +33 -0
- package/dist/proto-generator/index.d.ts +36 -0
- package/dist/proto-generator/index.js +250 -0
- package/docs/GRPC_USAGE.md +203 -0
- package/docs/INTEGRATION_EXAMPLE.md +364 -0
- package/examples/full-grpc-demo.ts +139 -0
- package/examples/grpc/calculator.proto +22 -0
- package/examples/grpc/fee.proto +60 -0
- package/examples/protos/hero.proto +17 -0
- package/package.json +26 -0
- package/src/grpc/client-factory.ts +48 -0
- package/src/grpc/errors.ts +31 -0
- package/src/grpc/server-factory.ts +263 -0
- package/src/grpc/types.ts +11 -0
- package/src/index.ts +42 -0
- package/src/proto-generator/index.ts +293 -0
- package/tests/fixtures/dummy.proto +15 -0
- package/tests/fixtures/no-package.proto +15 -0
- package/tests/grpc/client-factory.spec.ts +44 -0
- package/tests/grpc/errors.spec.ts +31 -0
- package/tests/index.spec.ts +29 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateProtoFromMethods = generateProtoFromMethods;
|
|
37
|
+
exports.generateProtoFromFunction = generateProtoFromFunction;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* Generate a complete .proto file from method definitions
|
|
42
|
+
*
|
|
43
|
+
* @param methods Array of method definitions
|
|
44
|
+
* @param options Generation options
|
|
45
|
+
* @returns The generated proto string
|
|
46
|
+
*/
|
|
47
|
+
function generateProtoFromMethods(methods, options = {}) {
|
|
48
|
+
const { packageName, serviceName = 'GeneratedService', outputDir, outputFilename } = options;
|
|
49
|
+
const messages = [];
|
|
50
|
+
const protoMethods = [];
|
|
51
|
+
const processedObjects = new Set();
|
|
52
|
+
// Helper: Capitalize string
|
|
53
|
+
function capitalize(s) {
|
|
54
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
55
|
+
}
|
|
56
|
+
// Helper: Convert camelCase to snake_case
|
|
57
|
+
function toSnakeCase(s) {
|
|
58
|
+
return s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
|
59
|
+
}
|
|
60
|
+
// Recursive function to analyze object and generate messages
|
|
61
|
+
function analyzeMessage(obj, msgName) {
|
|
62
|
+
if (obj instanceof Date)
|
|
63
|
+
return "string";
|
|
64
|
+
if (processedObjects.has(obj)) {
|
|
65
|
+
console.warn(`Circular reference detected for ${msgName}. Breaking recursion.`);
|
|
66
|
+
return "string";
|
|
67
|
+
}
|
|
68
|
+
processedObjects.add(obj);
|
|
69
|
+
const fields = [];
|
|
70
|
+
let fieldIdCounter = 1;
|
|
71
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
72
|
+
let type = "string";
|
|
73
|
+
let rule = undefined;
|
|
74
|
+
const valueType = typeof value;
|
|
75
|
+
if (value === null || value === undefined) {
|
|
76
|
+
type = "string";
|
|
77
|
+
}
|
|
78
|
+
else if (valueType === "string") {
|
|
79
|
+
type = "string";
|
|
80
|
+
}
|
|
81
|
+
else if (valueType === "boolean") {
|
|
82
|
+
type = "bool";
|
|
83
|
+
}
|
|
84
|
+
else if (valueType === "number") {
|
|
85
|
+
type = Number.isInteger(value) ? "int32" : "double";
|
|
86
|
+
}
|
|
87
|
+
else if (Array.isArray(value)) {
|
|
88
|
+
rule = "repeated";
|
|
89
|
+
if (value.length > 0) {
|
|
90
|
+
const firstItem = value[0];
|
|
91
|
+
if (typeof firstItem === "object") {
|
|
92
|
+
const nestedName = capitalize(key).replace(/s$/, "");
|
|
93
|
+
type = analyzeMessage(firstItem, nestedName);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
type = typeof firstItem === "number"
|
|
97
|
+
? (Number.isInteger(firstItem) ? "int32" : "double")
|
|
98
|
+
: typeof firstItem === "boolean" ? "bool" : "string";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
type = "string";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (valueType === "object") {
|
|
106
|
+
const nestedName = capitalize(key);
|
|
107
|
+
type = analyzeMessage(value, nestedName);
|
|
108
|
+
}
|
|
109
|
+
// Use snake_case for proto fields
|
|
110
|
+
fields.push({ name: toSnakeCase(key), type, rule, id: fieldIdCounter++ });
|
|
111
|
+
}
|
|
112
|
+
// Check if message already exists (same name)
|
|
113
|
+
const existingIndex = messages.findIndex(m => m.name === msgName);
|
|
114
|
+
if (existingIndex === -1) {
|
|
115
|
+
messages.push({ name: msgName, fields });
|
|
116
|
+
}
|
|
117
|
+
return msgName;
|
|
118
|
+
}
|
|
119
|
+
// Process each method
|
|
120
|
+
for (const method of methods) {
|
|
121
|
+
const requestTypeName = `${method.name}Request`;
|
|
122
|
+
const responseTypeName = `${method.name}Response`;
|
|
123
|
+
// Analyze request and response
|
|
124
|
+
const requestSample = method.requestSample();
|
|
125
|
+
const responseSample = method.responseSample();
|
|
126
|
+
analyzeMessage(requestSample, requestTypeName);
|
|
127
|
+
processedObjects.clear(); // Reset for next analysis
|
|
128
|
+
analyzeMessage(responseSample, responseTypeName);
|
|
129
|
+
processedObjects.clear();
|
|
130
|
+
protoMethods.push({
|
|
131
|
+
name: method.name,
|
|
132
|
+
requestType: requestTypeName,
|
|
133
|
+
responseType: responseTypeName,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Format the proto file
|
|
137
|
+
const lines = ['syntax = "proto3";', ''];
|
|
138
|
+
if (packageName) {
|
|
139
|
+
lines.push(`package ${packageName};`, '');
|
|
140
|
+
}
|
|
141
|
+
// Service definition
|
|
142
|
+
lines.push(`// ${serviceName} - Auto-generated gRPC service`);
|
|
143
|
+
lines.push(`service ${serviceName} {`);
|
|
144
|
+
for (const method of protoMethods) {
|
|
145
|
+
lines.push(` rpc ${method.name} (${method.requestType}) returns (${method.responseType}) {}`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('}', '');
|
|
148
|
+
// Message definitions (reverse for proper dependency order)
|
|
149
|
+
for (const msg of messages.reverse()) {
|
|
150
|
+
lines.push(`message ${msg.name} {`);
|
|
151
|
+
for (const field of msg.fields) {
|
|
152
|
+
const rulePrefix = field.rule ? `${field.rule} ` : "";
|
|
153
|
+
lines.push(` ${rulePrefix}${field.type} ${field.name} = ${field.id};`);
|
|
154
|
+
}
|
|
155
|
+
lines.push('}', '');
|
|
156
|
+
}
|
|
157
|
+
const protoContent = lines.join('\n');
|
|
158
|
+
// Save to file if outputDir is specified
|
|
159
|
+
if (outputDir) {
|
|
160
|
+
const filename = outputFilename || `${toSnakeCase(serviceName).replace(/_service$/, '')}.proto`;
|
|
161
|
+
const fullPath = path.join(outputDir, filename);
|
|
162
|
+
// Create directory if it doesn't exist
|
|
163
|
+
if (!fs.existsSync(outputDir)) {
|
|
164
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
fs.writeFileSync(fullPath, protoContent);
|
|
167
|
+
console.log(`✅ Proto file generated: ${fullPath}`);
|
|
168
|
+
}
|
|
169
|
+
return protoContent;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Convenience function for single-function proto generation (original API)
|
|
173
|
+
*/
|
|
174
|
+
function generateProtoFromFunction(generatorFn, rootMessageName = "RootMessage") {
|
|
175
|
+
const messages = [];
|
|
176
|
+
const processedObjects = new Set();
|
|
177
|
+
function capitalize(s) {
|
|
178
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
179
|
+
}
|
|
180
|
+
function toSnakeCase(s) {
|
|
181
|
+
return s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
|
182
|
+
}
|
|
183
|
+
function analyzeMessage(obj, msgName) {
|
|
184
|
+
if (obj instanceof Date)
|
|
185
|
+
return "string";
|
|
186
|
+
if (processedObjects.has(obj)) {
|
|
187
|
+
return "string";
|
|
188
|
+
}
|
|
189
|
+
processedObjects.add(obj);
|
|
190
|
+
const fields = [];
|
|
191
|
+
let fieldIdCounter = 1;
|
|
192
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
193
|
+
let type = "string";
|
|
194
|
+
let rule = undefined;
|
|
195
|
+
const valueType = typeof value;
|
|
196
|
+
if (value === null || value === undefined) {
|
|
197
|
+
type = "string";
|
|
198
|
+
}
|
|
199
|
+
else if (valueType === "string") {
|
|
200
|
+
type = "string";
|
|
201
|
+
}
|
|
202
|
+
else if (valueType === "boolean") {
|
|
203
|
+
type = "bool";
|
|
204
|
+
}
|
|
205
|
+
else if (valueType === "number") {
|
|
206
|
+
type = Number.isInteger(value) ? "int32" : "double";
|
|
207
|
+
}
|
|
208
|
+
else if (Array.isArray(value)) {
|
|
209
|
+
rule = "repeated";
|
|
210
|
+
if (value.length > 0) {
|
|
211
|
+
const firstItem = value[0];
|
|
212
|
+
if (typeof firstItem === "object") {
|
|
213
|
+
const nestedName = capitalize(key).replace(/s$/, "");
|
|
214
|
+
type = analyzeMessage(firstItem, nestedName);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
type = typeof firstItem === "number"
|
|
218
|
+
? (Number.isInteger(firstItem) ? "int32" : "double")
|
|
219
|
+
: typeof firstItem === "boolean" ? "bool" : "string";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
type = "string";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (valueType === "object") {
|
|
227
|
+
const nestedName = capitalize(key);
|
|
228
|
+
type = analyzeMessage(value, nestedName);
|
|
229
|
+
}
|
|
230
|
+
fields.push({ name: toSnakeCase(key), type, rule, id: fieldIdCounter++ });
|
|
231
|
+
}
|
|
232
|
+
messages.push({ name: msgName, fields });
|
|
233
|
+
return msgName;
|
|
234
|
+
}
|
|
235
|
+
const sampleData = generatorFn();
|
|
236
|
+
if (!sampleData || typeof sampleData !== "object") {
|
|
237
|
+
throw new Error("Generator function must return a non-null object.");
|
|
238
|
+
}
|
|
239
|
+
analyzeMessage(sampleData, rootMessageName);
|
|
240
|
+
const lines = ['syntax = "proto3";', ''];
|
|
241
|
+
for (const msg of messages.reverse()) {
|
|
242
|
+
lines.push(`message ${msg.name} {`);
|
|
243
|
+
for (const field of msg.fields) {
|
|
244
|
+
const rulePrefix = field.rule ? `${field.rule} ` : "";
|
|
245
|
+
lines.push(` ${rulePrefix}${field.type} ${field.name} = ${field.id};`);
|
|
246
|
+
}
|
|
247
|
+
lines.push('}', '');
|
|
248
|
+
}
|
|
249
|
+
return lines.join('\n');
|
|
250
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# @themainstack/communication - gRPC Usage Guide
|
|
2
|
+
|
|
3
|
+
A unified gRPC framework for inter-service communication at Mainstack.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
1. [Overview](#overview)
|
|
7
|
+
2. [Installation](#installation)
|
|
8
|
+
3. [Step 1: Proto Generation](#step-1-proto-generation)
|
|
9
|
+
4. [Step 2: Creating a gRPC Server](#step-2-creating-a-grpc-server)
|
|
10
|
+
5. [Step 3: Creating a gRPC Client](#step-3-creating-a-grpc-client)
|
|
11
|
+
6. [Step 4: Error Handling](#step-4-error-handling)
|
|
12
|
+
7. [Full Example](#full-example)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
The workflow is simple:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Developer writes TypeScript function
|
|
22
|
+
↓
|
|
23
|
+
Package auto-generates .proto file
|
|
24
|
+
↓
|
|
25
|
+
Server Factory exposes function as gRPC
|
|
26
|
+
↓
|
|
27
|
+
Client Factory in another service calls it
|
|
28
|
+
↓
|
|
29
|
+
Error Handler normalizes any errors
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @themainstack/communication
|
|
38
|
+
# or
|
|
39
|
+
yarn add @themainstack/communication
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Step 1: Proto Generation
|
|
45
|
+
|
|
46
|
+
Auto-generate `.proto` files from your TypeScript types.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { generateProtoFromMethods } from '@themainstack/communication';
|
|
50
|
+
|
|
51
|
+
generateProtoFromMethods([
|
|
52
|
+
{
|
|
53
|
+
name: 'CalculateFee',
|
|
54
|
+
requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
|
|
55
|
+
responseSample: () => ({ fee: 0, total: 0 }),
|
|
56
|
+
}
|
|
57
|
+
], {
|
|
58
|
+
packageName: 'fee.v1',
|
|
59
|
+
serviceName: 'FeeService',
|
|
60
|
+
outputDir: './src/grpc',
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Output:** `./src/grpc/feeservice.proto` is auto-generated!
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Step 2: Creating a gRPC Server
|
|
69
|
+
|
|
70
|
+
Expose existing functions as gRPC endpoints.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { GrpcServerFactory } from '@themainstack/communication';
|
|
74
|
+
|
|
75
|
+
// Your existing business function
|
|
76
|
+
async function calculateFee(request) {
|
|
77
|
+
return { fee: request.amount * 0.02, total: request.amount * 1.02 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create and start the server
|
|
81
|
+
const server = await GrpcServerFactory.createServer({
|
|
82
|
+
packageName: 'fee.v1',
|
|
83
|
+
serviceName: 'FeeService',
|
|
84
|
+
port: 50053,
|
|
85
|
+
}, [
|
|
86
|
+
{
|
|
87
|
+
name: 'CalculateFee',
|
|
88
|
+
handler: calculateFee, // Your existing function!
|
|
89
|
+
requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
|
|
90
|
+
responseSample: () => ({ fee: 0, total: 0 }),
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
await server.start();
|
|
95
|
+
// 🚀 gRPC Server running on 0.0.0.0:50053
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Quick Expose (One-liner)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { exposeAsGrpc } from '@themainstack/communication';
|
|
102
|
+
|
|
103
|
+
const server = await exposeAsGrpc(
|
|
104
|
+
'CalculateFee',
|
|
105
|
+
calculateFee,
|
|
106
|
+
{ requestSample: () => ({...}), responseSample: () => ({...}) },
|
|
107
|
+
{ packageName: 'fee.v1', serviceName: 'FeeService', port: 50053 }
|
|
108
|
+
);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Step 3: Creating a gRPC Client
|
|
114
|
+
|
|
115
|
+
Call gRPC services from other services.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { GrpcClientFactory } from '@themainstack/communication';
|
|
119
|
+
|
|
120
|
+
const client = GrpcClientFactory.createClient({
|
|
121
|
+
serviceName: 'FeeService',
|
|
122
|
+
packageName: 'fee.v1',
|
|
123
|
+
protoPath: './src/grpc/fee.proto',
|
|
124
|
+
url: process.env.FEE_SERVICE_URL || 'localhost:50053',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Make a call
|
|
128
|
+
client.CalculateFee(
|
|
129
|
+
{ merchantId: 'merchant_123', amount: 1000, currency: 'USD' },
|
|
130
|
+
(err, response) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
handleGrpcError(err);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log('Fee:', response.fee);
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Step 4: Error Handling
|
|
143
|
+
|
|
144
|
+
Standardized gRPC error translation.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { handleGrpcError } from '@themainstack/communication';
|
|
148
|
+
|
|
149
|
+
client.SomeMethod(request, (err, response) => {
|
|
150
|
+
if (err) {
|
|
151
|
+
try {
|
|
152
|
+
handleGrpcError(err); // Throws normalized error
|
|
153
|
+
} catch (normalizedError) {
|
|
154
|
+
console.error(normalizedError.message);
|
|
155
|
+
// Handle based on error type
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Process response
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Full Example
|
|
166
|
+
|
|
167
|
+
See `examples/full-grpc-demo.ts` for a complete working example that:
|
|
168
|
+
1. Defines a business function
|
|
169
|
+
2. Creates a gRPC server (proto auto-generated)
|
|
170
|
+
3. Creates a client and calls the server
|
|
171
|
+
4. Handles the response
|
|
172
|
+
|
|
173
|
+
Run it:
|
|
174
|
+
```bash
|
|
175
|
+
npx ts-node examples/full-grpc-demo.ts
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Environment Variables
|
|
181
|
+
|
|
182
|
+
| Variable | Description | Default |
|
|
183
|
+
|----------|-------------|---------|
|
|
184
|
+
| `GRPC_PORT` | Port for gRPC server | `50051` |
|
|
185
|
+
| `FEE_SERVICE_URL` | Fee service gRPC address | `localhost:50053` |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## API Reference
|
|
190
|
+
|
|
191
|
+
### Proto Generation
|
|
192
|
+
- `generateProtoFromMethods(methods, options)` - Generate proto with service definition
|
|
193
|
+
- `generateProtoFromFunction(fn, name)` - Generate proto for a single message
|
|
194
|
+
|
|
195
|
+
### Server
|
|
196
|
+
- `GrpcServerFactory.createServer(options, handlers)` - Create a gRPC server
|
|
197
|
+
- `exposeAsGrpc(name, handler, samples, options)` - Quick one-liner
|
|
198
|
+
|
|
199
|
+
### Client
|
|
200
|
+
- `GrpcClientFactory.createClient(options)` - Create a gRPC client
|
|
201
|
+
|
|
202
|
+
### Error Handling
|
|
203
|
+
- `handleGrpcError(err)` - Translate gRPC errors to application errors
|