@themainstack/communication 1.1.2 → 1.2.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/server-factory.d.ts +76 -12
- package/dist/grpc/server-factory.js +108 -35
- package/grpc/test.proto +15 -0
- package/package.json +1 -1
- package/src/grpc/server-factory.ts +160 -42
- package/tests/fixtures/calculator.proto +17 -0
- package/tests/fixtures/echo.proto +16 -0
- package/tests/multi-service.spec.ts +121 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Base options for creating a gRPC server (without service-specific options)
|
|
3
|
+
*/
|
|
4
|
+
export interface ServerBaseOptions {
|
|
5
|
+
/** Port to listen on (default: 50051) */
|
|
6
|
+
port?: number;
|
|
7
|
+
/** Host to bind to (default: '0.0.0.0') */
|
|
8
|
+
host?: string;
|
|
9
|
+
/** Directory to save auto-generated proto files (default: './grpc') */
|
|
10
|
+
protoOutputDir?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Service definition for adding a service to the server
|
|
14
|
+
*/
|
|
15
|
+
export interface ServiceDefinition {
|
|
16
|
+
/** The package name (e.g., 'fee.v1') */
|
|
17
|
+
packageName: string;
|
|
18
|
+
/** The service name (e.g., 'FeeService') */
|
|
19
|
+
serviceName: string;
|
|
20
|
+
/** Path to existing proto file (if not provided, will auto-generate) */
|
|
21
|
+
protoPath?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Options for creating a gRPC server (single-service, backward compatible)
|
|
3
25
|
*/
|
|
4
26
|
export interface GrpcServerOptions {
|
|
5
27
|
/** The package name (e.g., 'fee.v1') */
|
|
@@ -28,34 +50,71 @@ export interface ServerMethodHandler<TReq = any, TRes = any> {
|
|
|
28
50
|
/** Sample response for proto generation */
|
|
29
51
|
responseSample: () => TRes;
|
|
30
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Internal tracking of registered services
|
|
55
|
+
*/
|
|
56
|
+
interface RegisteredService {
|
|
57
|
+
packageName: string;
|
|
58
|
+
serviceName: string;
|
|
59
|
+
protoPath: string;
|
|
60
|
+
}
|
|
31
61
|
/**
|
|
32
62
|
* GrpcServerFactory - Create and run gRPC servers easily
|
|
33
63
|
*
|
|
34
|
-
*
|
|
64
|
+
* Single-service usage (backward compatible):
|
|
35
65
|
* ```typescript
|
|
36
66
|
* const server = await GrpcServerFactory.createServer({
|
|
37
67
|
* packageName: 'fee.v1',
|
|
38
68
|
* serviceName: 'FeeService',
|
|
39
69
|
* port: 50053,
|
|
40
|
-
* },
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
70
|
+
* }, handlers);
|
|
71
|
+
*
|
|
72
|
+
* await server.start();
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* Multi-service usage:
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const server = GrpcServerFactory.create({ port: 50051 });
|
|
78
|
+
*
|
|
79
|
+
* await server.addService(
|
|
80
|
+
* { packageName: 'fee.v1', serviceName: 'FeeService' },
|
|
81
|
+
* feeHandlers
|
|
82
|
+
* );
|
|
83
|
+
* await server.addService(
|
|
84
|
+
* { packageName: 'wallet.v1', serviceName: 'WalletService' },
|
|
85
|
+
* walletHandlers
|
|
86
|
+
* );
|
|
48
87
|
*
|
|
49
88
|
* await server.start();
|
|
50
89
|
* ```
|
|
51
90
|
*/
|
|
52
91
|
export declare class GrpcServerFactory {
|
|
53
92
|
private server;
|
|
54
|
-
private
|
|
93
|
+
private baseOptions;
|
|
94
|
+
private registeredServices;
|
|
55
95
|
private isRunning;
|
|
96
|
+
private options;
|
|
56
97
|
private constructor();
|
|
57
98
|
/**
|
|
58
|
-
* Create
|
|
99
|
+
* Create an empty gRPC server ready for adding multiple services
|
|
100
|
+
*
|
|
101
|
+
* Usage:
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const server = GrpcServerFactory.create({ port: 50051 });
|
|
104
|
+
* await server.addService({ packageName: 'fee.v1', serviceName: 'FeeService' }, handlers);
|
|
105
|
+
* await server.start();
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
static create(options?: ServerBaseOptions): GrpcServerFactory;
|
|
109
|
+
/**
|
|
110
|
+
* Add a service to this server
|
|
111
|
+
*
|
|
112
|
+
* @param service - Service definition (package name, service name, optional proto path)
|
|
113
|
+
* @param handlers - Method handlers for this service
|
|
114
|
+
*/
|
|
115
|
+
addService(service: ServiceDefinition, handlers: ServerMethodHandler[]): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Create a new gRPC server with the given handlers (single-service, backward compatible)
|
|
59
118
|
*/
|
|
60
119
|
static createServer(options: GrpcServerOptions, handlers: ServerMethodHandler[]): Promise<GrpcServerFactory>;
|
|
61
120
|
/**
|
|
@@ -78,6 +137,10 @@ export declare class GrpcServerFactory {
|
|
|
78
137
|
* Get the server address
|
|
79
138
|
*/
|
|
80
139
|
get address(): string;
|
|
140
|
+
/**
|
|
141
|
+
* Get list of registered services
|
|
142
|
+
*/
|
|
143
|
+
get services(): ReadonlyArray<RegisteredService>;
|
|
81
144
|
}
|
|
82
145
|
/**
|
|
83
146
|
* Helper function to quickly expose a function as a gRPC service
|
|
@@ -96,3 +159,4 @@ export declare function exposeAsGrpc<TReq extends object, TRes extends object>(m
|
|
|
96
159
|
requestSample: () => TReq;
|
|
97
160
|
responseSample: () => TRes;
|
|
98
161
|
}, options: GrpcServerOptions): Promise<GrpcServerFactory>;
|
|
162
|
+
export {};
|
|
@@ -43,43 +43,74 @@ const struct_utils_1 = require("./struct-utils");
|
|
|
43
43
|
/**
|
|
44
44
|
* GrpcServerFactory - Create and run gRPC servers easily
|
|
45
45
|
*
|
|
46
|
-
*
|
|
46
|
+
* Single-service usage (backward compatible):
|
|
47
47
|
* ```typescript
|
|
48
48
|
* const server = await GrpcServerFactory.createServer({
|
|
49
49
|
* packageName: 'fee.v1',
|
|
50
50
|
* serviceName: 'FeeService',
|
|
51
51
|
* port: 50053,
|
|
52
|
-
* },
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
52
|
+
* }, handlers);
|
|
53
|
+
*
|
|
54
|
+
* await server.start();
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Multi-service usage:
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const server = GrpcServerFactory.create({ port: 50051 });
|
|
60
|
+
*
|
|
61
|
+
* await server.addService(
|
|
62
|
+
* { packageName: 'fee.v1', serviceName: 'FeeService' },
|
|
63
|
+
* feeHandlers
|
|
64
|
+
* );
|
|
65
|
+
* await server.addService(
|
|
66
|
+
* { packageName: 'wallet.v1', serviceName: 'WalletService' },
|
|
67
|
+
* walletHandlers
|
|
68
|
+
* );
|
|
60
69
|
*
|
|
61
70
|
* await server.start();
|
|
62
71
|
* ```
|
|
63
72
|
*/
|
|
64
73
|
class GrpcServerFactory {
|
|
65
|
-
constructor(server,
|
|
74
|
+
constructor(server, baseOptions) {
|
|
75
|
+
this.registeredServices = [];
|
|
66
76
|
this.isRunning = false;
|
|
77
|
+
// For backward compatibility with single-service usage
|
|
78
|
+
this.options = null;
|
|
67
79
|
this.server = server;
|
|
68
|
-
this.
|
|
80
|
+
this.baseOptions = baseOptions;
|
|
69
81
|
}
|
|
70
82
|
/**
|
|
71
|
-
* Create
|
|
83
|
+
* Create an empty gRPC server ready for adding multiple services
|
|
84
|
+
*
|
|
85
|
+
* Usage:
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const server = GrpcServerFactory.create({ port: 50051 });
|
|
88
|
+
* await server.addService({ packageName: 'fee.v1', serviceName: 'FeeService' }, handlers);
|
|
89
|
+
* await server.start();
|
|
90
|
+
* ```
|
|
72
91
|
*/
|
|
73
|
-
static
|
|
92
|
+
static create(options = {}) {
|
|
74
93
|
const fullOptions = {
|
|
75
94
|
port: 50051,
|
|
76
95
|
host: '0.0.0.0',
|
|
77
96
|
protoOutputDir: './grpc',
|
|
78
|
-
protoPath: '',
|
|
79
97
|
...options,
|
|
80
98
|
};
|
|
99
|
+
const server = new grpc.Server();
|
|
100
|
+
return new GrpcServerFactory(server, fullOptions);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Add a service to this server
|
|
104
|
+
*
|
|
105
|
+
* @param service - Service definition (package name, service name, optional proto path)
|
|
106
|
+
* @param handlers - Method handlers for this service
|
|
107
|
+
*/
|
|
108
|
+
async addService(service, handlers) {
|
|
109
|
+
if (this.isRunning) {
|
|
110
|
+
throw new Error('Cannot add services to a running server. Call addService() before start().');
|
|
111
|
+
}
|
|
81
112
|
// Step 1: Generate or load proto file
|
|
82
|
-
let protoPath =
|
|
113
|
+
let protoPath = service.protoPath;
|
|
83
114
|
if (!protoPath) {
|
|
84
115
|
// Auto-generate proto from handlers
|
|
85
116
|
const methodDefinitions = handlers.map(h => ({
|
|
@@ -88,16 +119,16 @@ class GrpcServerFactory {
|
|
|
88
119
|
responseSample: h.responseSample,
|
|
89
120
|
}));
|
|
90
121
|
const protoContent = (0, proto_generator_1.generateProtoFromMethods)(methodDefinitions, {
|
|
91
|
-
packageName:
|
|
92
|
-
serviceName:
|
|
93
|
-
outputDir:
|
|
122
|
+
packageName: service.packageName,
|
|
123
|
+
serviceName: service.serviceName,
|
|
124
|
+
outputDir: this.baseOptions.protoOutputDir,
|
|
94
125
|
});
|
|
95
|
-
protoPath = path.join(
|
|
126
|
+
protoPath = path.join(this.baseOptions.protoOutputDir, `${service.serviceName.toLowerCase().replace(/service$/, '')}.proto`);
|
|
96
127
|
console.log(`Proto file auto-generated: ${protoPath}`);
|
|
97
128
|
}
|
|
98
129
|
// Step 2: Load the proto definition
|
|
99
130
|
const packageDefinition = protoLoader.loadSync(protoPath, {
|
|
100
|
-
keepCase: false,
|
|
131
|
+
keepCase: false,
|
|
101
132
|
longs: String,
|
|
102
133
|
enums: String,
|
|
103
134
|
defaults: true,
|
|
@@ -106,21 +137,20 @@ class GrpcServerFactory {
|
|
|
106
137
|
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
|
107
138
|
// Step 3: Navigate to the service definition
|
|
108
139
|
let serviceDefinition = protoDescriptor;
|
|
109
|
-
const packageParts =
|
|
140
|
+
const packageParts = service.packageName.split('.');
|
|
110
141
|
for (const part of packageParts) {
|
|
111
142
|
serviceDefinition = serviceDefinition[part];
|
|
112
143
|
if (!serviceDefinition) {
|
|
113
|
-
throw new Error(`Package '${
|
|
144
|
+
throw new Error(`Package '${service.packageName}' not found in proto`);
|
|
114
145
|
}
|
|
115
146
|
}
|
|
116
|
-
const ServiceConstructor = serviceDefinition[
|
|
147
|
+
const ServiceConstructor = serviceDefinition[service.serviceName];
|
|
117
148
|
if (!ServiceConstructor || !ServiceConstructor.service) {
|
|
118
|
-
throw new Error(`Service '${
|
|
149
|
+
throw new Error(`Service '${service.serviceName}' not found in package '${service.packageName}'`);
|
|
119
150
|
}
|
|
120
151
|
// Step 4: Create handler implementations
|
|
121
152
|
const implementations = {};
|
|
122
153
|
for (const handler of handlers) {
|
|
123
|
-
// gRPC uses lowercase first letter for method names in implementation
|
|
124
154
|
const methodName = handler.name.charAt(0).toLowerCase() + handler.name.slice(1);
|
|
125
155
|
implementations[methodName] = async (call, callback) => {
|
|
126
156
|
try {
|
|
@@ -130,7 +160,6 @@ class GrpcServerFactory {
|
|
|
130
160
|
if (methodDef && methodDef.requestType && methodDef.requestType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
131
161
|
for (const field of methodDef.requestType.type.field) {
|
|
132
162
|
if (field.typeName === 'google.protobuf.Struct' && request[field.name]) {
|
|
133
|
-
// Convert Struct -> JSON for the handler
|
|
134
163
|
request[field.name] = (0, struct_utils_1.structToJson)(request[field.name]);
|
|
135
164
|
}
|
|
136
165
|
}
|
|
@@ -140,7 +169,6 @@ class GrpcServerFactory {
|
|
|
140
169
|
if (result && typeof result === 'object' && methodDef && methodDef.responseType && methodDef.responseType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
141
170
|
for (const field of methodDef.responseType.type.field) {
|
|
142
171
|
if (field.typeName === 'google.protobuf.Struct' && result[field.name]) {
|
|
143
|
-
// Convert JSON -> Struct for the callback
|
|
144
172
|
result[field.name] = (0, struct_utils_1.jsonToStruct)(result[field.name]);
|
|
145
173
|
}
|
|
146
174
|
}
|
|
@@ -156,17 +184,53 @@ class GrpcServerFactory {
|
|
|
156
184
|
}
|
|
157
185
|
};
|
|
158
186
|
}
|
|
159
|
-
// Step 5:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
// Step 5: Add service to server
|
|
188
|
+
this.server.addService(ServiceConstructor.service, implementations);
|
|
189
|
+
// Track the registered service
|
|
190
|
+
this.registeredServices.push({
|
|
191
|
+
packageName: service.packageName,
|
|
192
|
+
serviceName: service.serviceName,
|
|
193
|
+
protoPath,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Create a new gRPC server with the given handlers (single-service, backward compatible)
|
|
198
|
+
*/
|
|
199
|
+
static async createServer(options, handlers) {
|
|
200
|
+
// Only pass defined values to avoid overriding defaults with undefined
|
|
201
|
+
const baseOptions = {};
|
|
202
|
+
if (options.port !== undefined)
|
|
203
|
+
baseOptions.port = options.port;
|
|
204
|
+
if (options.host !== undefined)
|
|
205
|
+
baseOptions.host = options.host;
|
|
206
|
+
if (options.protoOutputDir !== undefined)
|
|
207
|
+
baseOptions.protoOutputDir = options.protoOutputDir;
|
|
208
|
+
const factory = GrpcServerFactory.create(baseOptions);
|
|
209
|
+
await factory.addService({
|
|
210
|
+
packageName: options.packageName,
|
|
211
|
+
serviceName: options.serviceName,
|
|
212
|
+
protoPath: options.protoPath,
|
|
213
|
+
}, handlers);
|
|
214
|
+
// Store full options for backward compatibility (used in start() logging)
|
|
215
|
+
factory.options = {
|
|
216
|
+
port: factory.baseOptions.port,
|
|
217
|
+
host: factory.baseOptions.host,
|
|
218
|
+
protoOutputDir: factory.baseOptions.protoOutputDir,
|
|
219
|
+
protoPath: options.protoPath || '',
|
|
220
|
+
packageName: options.packageName,
|
|
221
|
+
serviceName: options.serviceName,
|
|
222
|
+
};
|
|
223
|
+
return factory;
|
|
163
224
|
}
|
|
164
225
|
/**
|
|
165
226
|
* Start the gRPC server
|
|
166
227
|
*/
|
|
167
228
|
async start() {
|
|
229
|
+
if (this.registeredServices.length === 0) {
|
|
230
|
+
throw new Error('No services registered. Call addService() before start().');
|
|
231
|
+
}
|
|
168
232
|
return new Promise((resolve, reject) => {
|
|
169
|
-
const address = `${this.
|
|
233
|
+
const address = `${this.baseOptions.host}:${this.baseOptions.port}`;
|
|
170
234
|
this.server.bindAsync(address, grpc.ServerCredentials.createInsecure(), (error, port) => {
|
|
171
235
|
if (error) {
|
|
172
236
|
reject(error);
|
|
@@ -174,8 +238,11 @@ class GrpcServerFactory {
|
|
|
174
238
|
}
|
|
175
239
|
this.isRunning = true;
|
|
176
240
|
console.log(` gRPC Server running on ${address}`);
|
|
177
|
-
|
|
178
|
-
|
|
241
|
+
// Log all registered services
|
|
242
|
+
for (const svc of this.registeredServices) {
|
|
243
|
+
console.log(` Service: ${svc.serviceName}`);
|
|
244
|
+
console.log(` Package: ${svc.packageName}`);
|
|
245
|
+
}
|
|
179
246
|
resolve();
|
|
180
247
|
});
|
|
181
248
|
});
|
|
@@ -210,7 +277,13 @@ class GrpcServerFactory {
|
|
|
210
277
|
* Get the server address
|
|
211
278
|
*/
|
|
212
279
|
get address() {
|
|
213
|
-
return `${this.
|
|
280
|
+
return `${this.baseOptions.host}:${this.baseOptions.port}`;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get list of registered services
|
|
284
|
+
*/
|
|
285
|
+
get services() {
|
|
286
|
+
return this.registeredServices;
|
|
214
287
|
}
|
|
215
288
|
}
|
|
216
289
|
exports.GrpcServerFactory = GrpcServerFactory;
|
package/grpc/test.proto
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package test.v1;
|
|
4
|
+
|
|
5
|
+
// TestService - Auto-generated gRPC service
|
|
6
|
+
service TestService {
|
|
7
|
+
rpc Ping (PingRequest) returns (PingResponse) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
message PingResponse {
|
|
11
|
+
bool pong = 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
message PingRequest {
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themainstack/communication",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Unified gRPC framework for inter-service communication - auto-generates protos, creates servers, and provides type-safe clients",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -6,7 +6,31 @@ import { generateProtoFromMethods, MethodDefinition } from '../proto-generator';
|
|
|
6
6
|
import { jsonToStruct, structToJson } from './struct-utils';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Base options for creating a gRPC server (without service-specific options)
|
|
10
|
+
*/
|
|
11
|
+
export interface ServerBaseOptions {
|
|
12
|
+
/** Port to listen on (default: 50051) */
|
|
13
|
+
port?: number;
|
|
14
|
+
/** Host to bind to (default: '0.0.0.0') */
|
|
15
|
+
host?: string;
|
|
16
|
+
/** Directory to save auto-generated proto files (default: './grpc') */
|
|
17
|
+
protoOutputDir?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Service definition for adding a service to the server
|
|
22
|
+
*/
|
|
23
|
+
export interface ServiceDefinition {
|
|
24
|
+
/** The package name (e.g., 'fee.v1') */
|
|
25
|
+
packageName: string;
|
|
26
|
+
/** The service name (e.g., 'FeeService') */
|
|
27
|
+
serviceName: string;
|
|
28
|
+
/** Path to existing proto file (if not provided, will auto-generate) */
|
|
29
|
+
protoPath?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for creating a gRPC server (single-service, backward compatible)
|
|
10
34
|
*/
|
|
11
35
|
export interface GrpcServerOptions {
|
|
12
36
|
/** The package name (e.g., 'fee.v1') */
|
|
@@ -37,57 +61,100 @@ export interface ServerMethodHandler<TReq = any, TRes = any> {
|
|
|
37
61
|
responseSample: () => TRes;
|
|
38
62
|
}
|
|
39
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Internal tracking of registered services
|
|
66
|
+
*/
|
|
67
|
+
interface RegisteredService {
|
|
68
|
+
packageName: string;
|
|
69
|
+
serviceName: string;
|
|
70
|
+
protoPath: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
/**
|
|
41
74
|
* GrpcServerFactory - Create and run gRPC servers easily
|
|
42
75
|
*
|
|
43
|
-
*
|
|
76
|
+
* Single-service usage (backward compatible):
|
|
44
77
|
* ```typescript
|
|
45
78
|
* const server = await GrpcServerFactory.createServer({
|
|
46
79
|
* packageName: 'fee.v1',
|
|
47
80
|
* serviceName: 'FeeService',
|
|
48
81
|
* port: 50053,
|
|
49
|
-
* },
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
82
|
+
* }, handlers);
|
|
83
|
+
*
|
|
84
|
+
* await server.start();
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* Multi-service usage:
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const server = GrpcServerFactory.create({ port: 50051 });
|
|
90
|
+
*
|
|
91
|
+
* await server.addService(
|
|
92
|
+
* { packageName: 'fee.v1', serviceName: 'FeeService' },
|
|
93
|
+
* feeHandlers
|
|
94
|
+
* );
|
|
95
|
+
* await server.addService(
|
|
96
|
+
* { packageName: 'wallet.v1', serviceName: 'WalletService' },
|
|
97
|
+
* walletHandlers
|
|
98
|
+
* );
|
|
57
99
|
*
|
|
58
100
|
* await server.start();
|
|
59
101
|
* ```
|
|
60
102
|
*/
|
|
61
103
|
export class GrpcServerFactory {
|
|
62
104
|
private server: grpc.Server;
|
|
63
|
-
private
|
|
105
|
+
private baseOptions: Required<ServerBaseOptions>;
|
|
106
|
+
private registeredServices: RegisteredService[] = [];
|
|
64
107
|
private isRunning: boolean = false;
|
|
65
108
|
|
|
109
|
+
// For backward compatibility with single-service usage
|
|
110
|
+
private options: Required<GrpcServerOptions> | null = null;
|
|
111
|
+
|
|
66
112
|
private constructor(
|
|
67
113
|
server: grpc.Server,
|
|
68
|
-
|
|
114
|
+
baseOptions: Required<ServerBaseOptions>
|
|
69
115
|
) {
|
|
70
116
|
this.server = server;
|
|
71
|
-
this.
|
|
117
|
+
this.baseOptions = baseOptions;
|
|
72
118
|
}
|
|
73
119
|
|
|
74
120
|
/**
|
|
75
|
-
* Create
|
|
121
|
+
* Create an empty gRPC server ready for adding multiple services
|
|
122
|
+
*
|
|
123
|
+
* Usage:
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const server = GrpcServerFactory.create({ port: 50051 });
|
|
126
|
+
* await server.addService({ packageName: 'fee.v1', serviceName: 'FeeService' }, handlers);
|
|
127
|
+
* await server.start();
|
|
128
|
+
* ```
|
|
76
129
|
*/
|
|
77
|
-
static
|
|
78
|
-
|
|
79
|
-
handlers: ServerMethodHandler[]
|
|
80
|
-
): Promise<GrpcServerFactory> {
|
|
81
|
-
const fullOptions: Required<GrpcServerOptions> = {
|
|
130
|
+
static create(options: ServerBaseOptions = {}): GrpcServerFactory {
|
|
131
|
+
const fullOptions: Required<ServerBaseOptions> = {
|
|
82
132
|
port: 50051,
|
|
83
133
|
host: '0.0.0.0',
|
|
84
134
|
protoOutputDir: './grpc',
|
|
85
|
-
protoPath: '',
|
|
86
135
|
...options,
|
|
87
136
|
};
|
|
88
137
|
|
|
138
|
+
const server = new grpc.Server();
|
|
139
|
+
return new GrpcServerFactory(server, fullOptions);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Add a service to this server
|
|
144
|
+
*
|
|
145
|
+
* @param service - Service definition (package name, service name, optional proto path)
|
|
146
|
+
* @param handlers - Method handlers for this service
|
|
147
|
+
*/
|
|
148
|
+
async addService(
|
|
149
|
+
service: ServiceDefinition,
|
|
150
|
+
handlers: ServerMethodHandler[]
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
if (this.isRunning) {
|
|
153
|
+
throw new Error('Cannot add services to a running server. Call addService() before start().');
|
|
154
|
+
}
|
|
155
|
+
|
|
89
156
|
// Step 1: Generate or load proto file
|
|
90
|
-
let protoPath =
|
|
157
|
+
let protoPath = service.protoPath;
|
|
91
158
|
|
|
92
159
|
if (!protoPath) {
|
|
93
160
|
// Auto-generate proto from handlers
|
|
@@ -98,14 +165,14 @@ export class GrpcServerFactory {
|
|
|
98
165
|
}));
|
|
99
166
|
|
|
100
167
|
const protoContent = generateProtoFromMethods(methodDefinitions, {
|
|
101
|
-
packageName:
|
|
102
|
-
serviceName:
|
|
103
|
-
outputDir:
|
|
168
|
+
packageName: service.packageName,
|
|
169
|
+
serviceName: service.serviceName,
|
|
170
|
+
outputDir: this.baseOptions.protoOutputDir,
|
|
104
171
|
});
|
|
105
172
|
|
|
106
173
|
protoPath = path.join(
|
|
107
|
-
|
|
108
|
-
`${
|
|
174
|
+
this.baseOptions.protoOutputDir,
|
|
175
|
+
`${service.serviceName.toLowerCase().replace(/service$/, '')}.proto`
|
|
109
176
|
);
|
|
110
177
|
|
|
111
178
|
console.log(`Proto file auto-generated: ${protoPath}`);
|
|
@@ -113,7 +180,7 @@ export class GrpcServerFactory {
|
|
|
113
180
|
|
|
114
181
|
// Step 2: Load the proto definition
|
|
115
182
|
const packageDefinition = protoLoader.loadSync(protoPath, {
|
|
116
|
-
keepCase: false,
|
|
183
|
+
keepCase: false,
|
|
117
184
|
longs: String,
|
|
118
185
|
enums: String,
|
|
119
186
|
defaults: true,
|
|
@@ -124,24 +191,23 @@ export class GrpcServerFactory {
|
|
|
124
191
|
|
|
125
192
|
// Step 3: Navigate to the service definition
|
|
126
193
|
let serviceDefinition: any = protoDescriptor;
|
|
127
|
-
const packageParts =
|
|
194
|
+
const packageParts = service.packageName.split('.');
|
|
128
195
|
for (const part of packageParts) {
|
|
129
196
|
serviceDefinition = serviceDefinition[part];
|
|
130
197
|
if (!serviceDefinition) {
|
|
131
|
-
throw new Error(`Package '${
|
|
198
|
+
throw new Error(`Package '${service.packageName}' not found in proto`);
|
|
132
199
|
}
|
|
133
200
|
}
|
|
134
201
|
|
|
135
|
-
const ServiceConstructor = serviceDefinition[
|
|
202
|
+
const ServiceConstructor = serviceDefinition[service.serviceName];
|
|
136
203
|
if (!ServiceConstructor || !ServiceConstructor.service) {
|
|
137
|
-
throw new Error(`Service '${
|
|
204
|
+
throw new Error(`Service '${service.serviceName}' not found in package '${service.packageName}'`);
|
|
138
205
|
}
|
|
139
206
|
|
|
140
207
|
// Step 4: Create handler implementations
|
|
141
208
|
const implementations: Record<string, grpc.handleUnaryCall<any, any>> = {};
|
|
142
209
|
|
|
143
210
|
for (const handler of handlers) {
|
|
144
|
-
// gRPC uses lowercase first letter for method names in implementation
|
|
145
211
|
const methodName = handler.name.charAt(0).toLowerCase() + handler.name.slice(1);
|
|
146
212
|
|
|
147
213
|
implementations[methodName] = async (
|
|
@@ -156,7 +222,6 @@ export class GrpcServerFactory {
|
|
|
156
222
|
if (methodDef && methodDef.requestType && methodDef.requestType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
157
223
|
for (const field of methodDef.requestType.type.field) {
|
|
158
224
|
if (field.typeName === 'google.protobuf.Struct' && request[field.name]) {
|
|
159
|
-
// Convert Struct -> JSON for the handler
|
|
160
225
|
request[field.name] = structToJson(request[field.name]);
|
|
161
226
|
}
|
|
162
227
|
}
|
|
@@ -168,7 +233,6 @@ export class GrpcServerFactory {
|
|
|
168
233
|
if (result && typeof result === 'object' && methodDef && methodDef.responseType && methodDef.responseType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
169
234
|
for (const field of methodDef.responseType.type.field) {
|
|
170
235
|
if (field.typeName === 'google.protobuf.Struct' && result[field.name]) {
|
|
171
|
-
// Convert JSON -> Struct for the callback
|
|
172
236
|
result[field.name] = jsonToStruct(result[field.name]);
|
|
173
237
|
}
|
|
174
238
|
}
|
|
@@ -185,19 +249,61 @@ export class GrpcServerFactory {
|
|
|
185
249
|
};
|
|
186
250
|
}
|
|
187
251
|
|
|
188
|
-
// Step 5:
|
|
189
|
-
|
|
190
|
-
|
|
252
|
+
// Step 5: Add service to server
|
|
253
|
+
this.server.addService(ServiceConstructor.service, implementations);
|
|
254
|
+
|
|
255
|
+
// Track the registered service
|
|
256
|
+
this.registeredServices.push({
|
|
257
|
+
packageName: service.packageName,
|
|
258
|
+
serviceName: service.serviceName,
|
|
259
|
+
protoPath,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
191
262
|
|
|
192
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Create a new gRPC server with the given handlers (single-service, backward compatible)
|
|
265
|
+
*/
|
|
266
|
+
static async createServer(
|
|
267
|
+
options: GrpcServerOptions,
|
|
268
|
+
handlers: ServerMethodHandler[]
|
|
269
|
+
): Promise<GrpcServerFactory> {
|
|
270
|
+
// Only pass defined values to avoid overriding defaults with undefined
|
|
271
|
+
const baseOptions: ServerBaseOptions = {};
|
|
272
|
+
if (options.port !== undefined) baseOptions.port = options.port;
|
|
273
|
+
if (options.host !== undefined) baseOptions.host = options.host;
|
|
274
|
+
if (options.protoOutputDir !== undefined) baseOptions.protoOutputDir = options.protoOutputDir;
|
|
275
|
+
|
|
276
|
+
const factory = GrpcServerFactory.create(baseOptions);
|
|
277
|
+
|
|
278
|
+
await factory.addService({
|
|
279
|
+
packageName: options.packageName,
|
|
280
|
+
serviceName: options.serviceName,
|
|
281
|
+
protoPath: options.protoPath,
|
|
282
|
+
}, handlers);
|
|
283
|
+
|
|
284
|
+
// Store full options for backward compatibility (used in start() logging)
|
|
285
|
+
factory.options = {
|
|
286
|
+
port: factory.baseOptions.port,
|
|
287
|
+
host: factory.baseOptions.host,
|
|
288
|
+
protoOutputDir: factory.baseOptions.protoOutputDir,
|
|
289
|
+
protoPath: options.protoPath || '',
|
|
290
|
+
packageName: options.packageName,
|
|
291
|
+
serviceName: options.serviceName,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return factory;
|
|
193
295
|
}
|
|
194
296
|
|
|
195
297
|
/**
|
|
196
298
|
* Start the gRPC server
|
|
197
299
|
*/
|
|
198
300
|
async start(): Promise<void> {
|
|
301
|
+
if (this.registeredServices.length === 0) {
|
|
302
|
+
throw new Error('No services registered. Call addService() before start().');
|
|
303
|
+
}
|
|
304
|
+
|
|
199
305
|
return new Promise((resolve, reject) => {
|
|
200
|
-
const address = `${this.
|
|
306
|
+
const address = `${this.baseOptions.host}:${this.baseOptions.port}`;
|
|
201
307
|
|
|
202
308
|
this.server.bindAsync(
|
|
203
309
|
address,
|
|
@@ -210,8 +316,13 @@ export class GrpcServerFactory {
|
|
|
210
316
|
|
|
211
317
|
this.isRunning = true;
|
|
212
318
|
console.log(` gRPC Server running on ${address}`);
|
|
213
|
-
|
|
214
|
-
|
|
319
|
+
|
|
320
|
+
// Log all registered services
|
|
321
|
+
for (const svc of this.registeredServices) {
|
|
322
|
+
console.log(` Service: ${svc.serviceName}`);
|
|
323
|
+
console.log(` Package: ${svc.packageName}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
215
326
|
resolve();
|
|
216
327
|
}
|
|
217
328
|
);
|
|
@@ -251,7 +362,14 @@ export class GrpcServerFactory {
|
|
|
251
362
|
* Get the server address
|
|
252
363
|
*/
|
|
253
364
|
get address(): string {
|
|
254
|
-
return `${this.
|
|
365
|
+
return `${this.baseOptions.host}:${this.baseOptions.port}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get list of registered services
|
|
370
|
+
*/
|
|
371
|
+
get services(): ReadonlyArray<RegisteredService> {
|
|
372
|
+
return this.registeredServices;
|
|
255
373
|
}
|
|
256
374
|
}
|
|
257
375
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package calc.v1;
|
|
4
|
+
|
|
5
|
+
// CalculatorService - Auto-generated gRPC service
|
|
6
|
+
service CalculatorService {
|
|
7
|
+
rpc Add (AddRequest) returns (AddResponse) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
message AddResponse {
|
|
11
|
+
int32 result = 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
message AddRequest {
|
|
15
|
+
int32 a = 1;
|
|
16
|
+
int32 b = 2;
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package echo.v1;
|
|
4
|
+
|
|
5
|
+
// EchoService - Auto-generated gRPC service
|
|
6
|
+
service EchoService {
|
|
7
|
+
rpc Echo (EchoRequest) returns (EchoResponse) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
message EchoResponse {
|
|
11
|
+
string reply = 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
message EchoRequest {
|
|
15
|
+
string message = 1;
|
|
16
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { GrpcServerFactory, GrpcClientFactory } from '../src/index';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
|
|
6
|
+
describe('Multi-Service Support', () => {
|
|
7
|
+
it('should support multiple services on one server', async () => {
|
|
8
|
+
const PORT = 50059;
|
|
9
|
+
const FIXTURES_DIR = path.join(__dirname, 'fixtures');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(FIXTURES_DIR)) {
|
|
12
|
+
fs.mkdirSync(FIXTURES_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Create empty server
|
|
16
|
+
const server = GrpcServerFactory.create({
|
|
17
|
+
port: PORT,
|
|
18
|
+
protoOutputDir: FIXTURES_DIR,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Add first service: Calculator
|
|
22
|
+
await server.addService(
|
|
23
|
+
{ packageName: 'calc.v1', serviceName: 'CalculatorService' },
|
|
24
|
+
[{
|
|
25
|
+
name: 'Add',
|
|
26
|
+
handler: async (req: { a: number; b: number }) => ({ result: req.a + req.b }),
|
|
27
|
+
requestSample: () => ({ a: 0, b: 0 }),
|
|
28
|
+
responseSample: () => ({ result: 0 }),
|
|
29
|
+
}]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Add second service: Echo
|
|
33
|
+
await server.addService(
|
|
34
|
+
{ packageName: 'echo.v1', serviceName: 'EchoService' },
|
|
35
|
+
[{
|
|
36
|
+
name: 'Echo',
|
|
37
|
+
handler: async (req: { message: string }) => ({ reply: `Echo: ${req.message}` }),
|
|
38
|
+
requestSample: () => ({ message: '' }),
|
|
39
|
+
responseSample: () => ({ reply: '' }),
|
|
40
|
+
}]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Verify services are registered
|
|
44
|
+
expect(server.services).toHaveLength(2);
|
|
45
|
+
expect(server.services[0].serviceName).toBe('CalculatorService');
|
|
46
|
+
expect(server.services[1].serviceName).toBe('EchoService');
|
|
47
|
+
|
|
48
|
+
// Start server
|
|
49
|
+
await server.start();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Create client for Calculator
|
|
53
|
+
const calcClient = GrpcClientFactory.createClient<any>({
|
|
54
|
+
packageName: 'calc.v1',
|
|
55
|
+
serviceName: 'CalculatorService',
|
|
56
|
+
protoPath: path.join(FIXTURES_DIR, 'calculator.proto'),
|
|
57
|
+
url: `0.0.0.0:${PORT}`,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Create client for Echo
|
|
61
|
+
const echoClient = GrpcClientFactory.createClient<any>({
|
|
62
|
+
packageName: 'echo.v1',
|
|
63
|
+
serviceName: 'EchoService',
|
|
64
|
+
protoPath: path.join(FIXTURES_DIR, 'echo.proto'),
|
|
65
|
+
url: `0.0.0.0:${PORT}`,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Test Calculator service
|
|
69
|
+
const calcResult = await new Promise((resolve, reject) => {
|
|
70
|
+
calcClient.Add({ a: 5, b: 3 }, (err: any, res: any) => {
|
|
71
|
+
if (err) reject(err);
|
|
72
|
+
else resolve(res);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
expect((calcResult as any).result).toBe(8);
|
|
76
|
+
|
|
77
|
+
// Test Echo service
|
|
78
|
+
const echoResult = await new Promise((resolve, reject) => {
|
|
79
|
+
echoClient.Echo({ message: 'hello' }, (err: any, res: any) => {
|
|
80
|
+
if (err) reject(err);
|
|
81
|
+
else resolve(res);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
expect((echoResult as any).reply).toBe('Echo: hello');
|
|
85
|
+
|
|
86
|
+
} finally {
|
|
87
|
+
server.stop();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should throw if addService is called after start', async () => {
|
|
92
|
+
const server = GrpcServerFactory.create({ port: 50060 });
|
|
93
|
+
|
|
94
|
+
await server.addService(
|
|
95
|
+
{ packageName: 'test.v1', serviceName: 'TestService' },
|
|
96
|
+
[{
|
|
97
|
+
name: 'Ping',
|
|
98
|
+
handler: async () => ({ pong: true }),
|
|
99
|
+
requestSample: () => ({}),
|
|
100
|
+
responseSample: () => ({ pong: true }),
|
|
101
|
+
}]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
await server.start();
|
|
105
|
+
|
|
106
|
+
await expect(
|
|
107
|
+
server.addService(
|
|
108
|
+
{ packageName: 'test2.v1', serviceName: 'Test2Service' },
|
|
109
|
+
[{ name: 'Ping', handler: async () => ({}), requestSample: () => ({}), responseSample: () => ({}) }]
|
|
110
|
+
)
|
|
111
|
+
).rejects.toThrow('Cannot add services to a running server');
|
|
112
|
+
|
|
113
|
+
server.stop();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should throw if start is called without services', async () => {
|
|
117
|
+
const server = GrpcServerFactory.create({ port: 50061 });
|
|
118
|
+
|
|
119
|
+
await expect(server.start()).rejects.toThrow('No services registered');
|
|
120
|
+
});
|
|
121
|
+
});
|