@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.
@@ -1,5 +1,27 @@
1
1
  /**
2
- * Options for creating a gRPC server
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
- * Usage:
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
- * name: 'CalculateWithdrawalFee',
43
- * handler: async (req) => calculateFee(req),
44
- * requestSample: () => ({ merchantId: '', amount: 0 }),
45
- * responseSample: () => ({ dollarAmount: 0, fees: [] }),
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 options;
93
+ private baseOptions;
94
+ private registeredServices;
55
95
  private isRunning;
96
+ private options;
56
97
  private constructor();
57
98
  /**
58
- * Create a new gRPC server with the given handlers
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
- * Usage:
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
- * name: 'CalculateWithdrawalFee',
55
- * handler: async (req) => calculateFee(req),
56
- * requestSample: () => ({ merchantId: '', amount: 0 }),
57
- * responseSample: () => ({ dollarAmount: 0, fees: [] }),
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, options) {
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.options = options;
80
+ this.baseOptions = baseOptions;
69
81
  }
70
82
  /**
71
- * Create a new gRPC server with the given handlers
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 async createServer(options, handlers) {
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 = fullOptions.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: fullOptions.packageName,
92
- serviceName: fullOptions.serviceName,
93
- outputDir: fullOptions.protoOutputDir,
122
+ packageName: service.packageName,
123
+ serviceName: service.serviceName,
124
+ outputDir: this.baseOptions.protoOutputDir,
94
125
  });
95
- protoPath = path.join(fullOptions.protoOutputDir, `${fullOptions.serviceName.toLowerCase().replace(/service$/, '')}.proto`);
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, // Convert snake_case to camelCase
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 = fullOptions.packageName.split('.');
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 '${fullOptions.packageName}' not found in proto`);
144
+ throw new Error(`Package '${service.packageName}' not found in proto`);
114
145
  }
115
146
  }
116
- const ServiceConstructor = serviceDefinition[fullOptions.serviceName];
147
+ const ServiceConstructor = serviceDefinition[service.serviceName];
117
148
  if (!ServiceConstructor || !ServiceConstructor.service) {
118
- throw new Error(`Service '${fullOptions.serviceName}' not found in package '${fullOptions.packageName}'`);
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: Create and configure the server
160
- const server = new grpc.Server();
161
- server.addService(ServiceConstructor.service, implementations);
162
- return new GrpcServerFactory(server, { ...fullOptions, protoPath });
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.options.host}:${this.options.port}`;
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
- console.log(` Service: ${this.options.serviceName}`);
178
- console.log(` Package: ${this.options.packageName}`);
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.options.host}:${this.options.port}`;
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;
@@ -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.1.2",
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
- * Options for creating a gRPC server
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
- * Usage:
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
- * name: 'CalculateWithdrawalFee',
52
- * handler: async (req) => calculateFee(req),
53
- * requestSample: () => ({ merchantId: '', amount: 0 }),
54
- * responseSample: () => ({ dollarAmount: 0, fees: [] }),
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 options: Required<GrpcServerOptions>;
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
- options: Required<GrpcServerOptions>
114
+ baseOptions: Required<ServerBaseOptions>
69
115
  ) {
70
116
  this.server = server;
71
- this.options = options;
117
+ this.baseOptions = baseOptions;
72
118
  }
73
119
 
74
120
  /**
75
- * Create a new gRPC server with the given handlers
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 async createServer(
78
- options: GrpcServerOptions,
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 = fullOptions.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: fullOptions.packageName,
102
- serviceName: fullOptions.serviceName,
103
- outputDir: fullOptions.protoOutputDir,
168
+ packageName: service.packageName,
169
+ serviceName: service.serviceName,
170
+ outputDir: this.baseOptions.protoOutputDir,
104
171
  });
105
172
 
106
173
  protoPath = path.join(
107
- fullOptions.protoOutputDir,
108
- `${fullOptions.serviceName.toLowerCase().replace(/service$/, '')}.proto`
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, // Convert snake_case to camelCase
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 = fullOptions.packageName.split('.');
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 '${fullOptions.packageName}' not found in proto`);
198
+ throw new Error(`Package '${service.packageName}' not found in proto`);
132
199
  }
133
200
  }
134
201
 
135
- const ServiceConstructor = serviceDefinition[fullOptions.serviceName];
202
+ const ServiceConstructor = serviceDefinition[service.serviceName];
136
203
  if (!ServiceConstructor || !ServiceConstructor.service) {
137
- throw new Error(`Service '${fullOptions.serviceName}' not found in package '${fullOptions.packageName}'`);
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: Create and configure the server
189
- const server = new grpc.Server();
190
- server.addService(ServiceConstructor.service, implementations);
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
- return new GrpcServerFactory(server, { ...fullOptions, protoPath });
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.options.host}:${this.options.port}`;
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
- console.log(` Service: ${this.options.serviceName}`);
214
- console.log(` Package: ${this.options.packageName}`);
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.options.host}:${this.options.port}`;
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
+ });