Skip to content

Commit 004395c

Browse files
committed
Implement detection of available protocol and creation of gRPC client
1 parent 2c79413 commit 004395c

File tree

2 files changed

+169
-39
lines changed

2 files changed

+169
-39
lines changed

src/client.ts

Lines changed: 168 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import {ChannelCredentials, Metadata, ServiceError} from '@postman/grpc-js';
2-
import {getDescriptorRootFromDescriptorSet} from './descriptor';
3-
import * as services from './reflection_grpc_pb';
41
import {
5-
ServerReflectionRequest,
6-
ServerReflectionResponse,
7-
} from './reflection_pb';
2+
ChannelCredentials,
3+
Metadata,
4+
ServiceError,
5+
status as GrpcStatus,
6+
} from '@postman/grpc-js';
7+
import {getDescriptorRootFromDescriptorSet} from './descriptor';
88
import {Root} from '@postman/protobufjs';
99
import {
1010
FileDescriptorSet,
@@ -13,26 +13,147 @@ import {
1313
} from '@postman/protobufjs/ext/descriptor';
1414
import set from 'lodash.set';
1515

16+
// Static type definitions with common structures across all reflection providers
17+
import type {ServerReflectionClient} from './reflection_providers/v1alpha/reflection_grpc_pb';
18+
import type {
19+
ServerReflectionRequest,
20+
ServerReflectionResponse,
21+
} from './reflection_providers/v1alpha/reflection_pb';
22+
23+
const supportedReflectionProtocols = [
24+
{
25+
protocol: 'v1alpha',
26+
serviceName: 'grpc.reflection.v1alpha.ServerReflection',
27+
client: import('./reflection_providers/v1alpha/reflection_pb'),
28+
service: import('./reflection_providers/v1alpha/reflection_grpc_pb'),
29+
},
30+
{
31+
protocol: 'v1',
32+
serviceName: 'grpc.reflection.v1.ServerReflection',
33+
client: import('./reflection_providers/v1/reflection_pb'),
34+
service: import('./reflection_providers/v1/reflection_grpc_pb'),
35+
},
36+
];
37+
1638
export class Client {
1739
metadata: Metadata;
18-
grpcClient: services.IServerReflectionClient;
1940
private fileDescriptorCache: Map<string, IFileDescriptorProto> = new Map();
41+
private url: string;
42+
private credentials: ChannelCredentials;
43+
private clientOptions: object | undefined;
44+
45+
grpcClient: ServerReflectionClient | undefined;
46+
private reflectionResponseCache: ServerReflectionResponse | undefined;
47+
private compatibleProtocol: string | undefined;
48+
private CompatibleServerReflectionRequest:
49+
| (new (
50+
...args: ConstructorParameters<typeof ServerReflectionRequest>
51+
) => ServerReflectionRequest)
52+
| undefined;
53+
2054
constructor(
2155
url: string,
2256
credentials: ChannelCredentials,
2357
options?: object,
2458
metadata?: Metadata
2559
) {
60+
this.url = url;
61+
this.credentials = credentials;
62+
this.clientOptions = options;
2663
this.fileDescriptorCache = new Map();
2764
this.metadata = metadata || new Metadata();
28-
this.grpcClient = new services.ServerReflectionClient(
29-
url,
30-
credentials,
31-
options
32-
);
3365
}
3466

35-
listServices(): Promise<string[]> {
67+
private async sendReflectionRequest(
68+
request: ServerReflectionRequest | ServerReflectionRequest[],
69+
client?: ServerReflectionClient
70+
): Promise<ServerReflectionResponse[]> {
71+
return new Promise((resolve, reject) => {
72+
const result: ServerReflectionResponse[] = [];
73+
74+
const grpcCall = (client || this.grpcClient!).serverReflectionInfo(
75+
this.metadata
76+
);
77+
78+
grpcCall.on('data', (response: ServerReflectionResponse) => {
79+
result.push(response);
80+
});
81+
82+
grpcCall.on('error', (error: ServiceError) => {
83+
reject(error);
84+
});
85+
86+
grpcCall.on('end', () => resolve(result));
87+
88+
if (Array.isArray(request)) {
89+
request.forEach(req => grpcCall.write(req));
90+
} else {
91+
grpcCall.write(request);
92+
}
93+
94+
grpcCall.end();
95+
});
96+
}
97+
98+
private async initializeReflectionClient() {
99+
if (this.grpcClient || this.compatibleProtocol) {
100+
return;
101+
}
102+
103+
// Check protocol compatibility and initialize gRPC client based on that
104+
for (const protocolConfig of supportedReflectionProtocols) {
105+
const {
106+
protocol,
107+
service: servicePromise,
108+
client: clientPromise,
109+
} = protocolConfig;
110+
111+
const [protocolService, protocolClient] = await Promise.all([
112+
servicePromise,
113+
clientPromise,
114+
]);
115+
116+
const grpcClientForProtocol = new protocolService.ServerReflectionClient(
117+
this.url,
118+
this.credentials,
119+
this.clientOptions
120+
);
121+
122+
const request = new protocolClient.ServerReflectionRequest();
123+
124+
request.setListServices('*');
125+
126+
try {
127+
const [reflectionResponse] = await this.sendReflectionRequest(
128+
request,
129+
grpcClientForProtocol
130+
);
131+
132+
this.grpcClient = grpcClientForProtocol;
133+
this.compatibleProtocol = protocol;
134+
this.CompatibleServerReflectionRequest =
135+
protocolClient.ServerReflectionRequest;
136+
this.reflectionResponseCache = reflectionResponse;
137+
138+
break; // Exit loop on first successful protocol
139+
} catch (error) {
140+
if ((error as ServiceError).code !== GrpcStatus.UNIMPLEMENTED) {
141+
// Something is wrong with the gRPC server itself
142+
throw error;
143+
}
144+
145+
continue; // Try next protocol
146+
}
147+
}
148+
149+
if (!this.grpcClient) {
150+
throw new Error('No compatible reflection protocol found.');
151+
}
152+
}
153+
154+
async listServices(): Promise<string[]> {
155+
await this.initializeReflectionClient();
156+
36157
return new Promise((resolve, reject) => {
37158
function dataCallback(response: ServerReflectionResponse) {
38159
if (response.hasListServicesResponse()) {
@@ -52,14 +173,16 @@ export class Client {
52173
reject(e);
53174
}
54175

55-
const request = new ServerReflectionRequest();
176+
if (this.reflectionResponseCache) {
177+
return dataCallback(this.reflectionResponseCache);
178+
}
179+
180+
const request = new this.CompatibleServerReflectionRequest!();
56181
request.setListServices('*');
57182

58-
const grpcCall = this.grpcClient.serverReflectionInfo(this.metadata);
59-
grpcCall.on('data', dataCallback);
60-
grpcCall.on('error', errorCallback);
61-
grpcCall.write(request);
62-
grpcCall.end();
183+
this.sendReflectionRequest(request)
184+
.then(([response]) => dataCallback(response))
185+
.catch(errorCallback);
63186
});
64187
}
65188

@@ -120,9 +243,11 @@ export class Client {
120243
return fileDescriptorMap;
121244
}
122245

123-
private getFileContainingSymbol(
246+
private async getFileContainingSymbol(
124247
symbol: string
125248
): Promise<Array<IFileDescriptorProto> | undefined> {
249+
await this.initializeReflectionClient();
250+
126251
const fileDescriptorCache = this.fileDescriptorCache;
127252
return new Promise((resolve, reject) => {
128253
function dataCallback(response: ServerReflectionResponse) {
@@ -154,20 +279,20 @@ export class Client {
154279
reject(e);
155280
}
156281

157-
const request = new ServerReflectionRequest();
282+
const request = new this.CompatibleServerReflectionRequest!();
158283
request.setFileContainingSymbol(symbol);
159284

160-
const grpcCall = this.grpcClient.serverReflectionInfo(this.metadata);
161-
grpcCall.on('data', dataCallback);
162-
grpcCall.on('error', errorCallback);
163-
grpcCall.write(request);
164-
grpcCall.end();
285+
this.sendReflectionRequest(request)
286+
.then(([response]) => dataCallback(response))
287+
.catch(errorCallback);
165288
});
166289
}
167290

168-
private getFilesByFilenames(
291+
private async getFilesByFilenames(
169292
symbols: string[]
170293
): Promise<Array<IFileDescriptorProto> | undefined> {
294+
await this.initializeReflectionClient();
295+
171296
const result: Array<IFileDescriptorProto> = [];
172297
const fileDescriptorCache = this.fileDescriptorCache;
173298
const symbolsToFetch = symbols.filter(symbol => {
@@ -203,6 +328,13 @@ export class Client {
203328
result.push(fileDescriptorProto);
204329
}
205330
});
331+
} else if (response.hasErrorResponse()) {
332+
const err = response.getErrorResponse();
333+
reject(
334+
new Error(
335+
`Error: ${err?.getErrorCode()}: ${err?.getErrorMessage()}`
336+
)
337+
);
206338
} else {
207339
reject(Error());
208340
}
@@ -212,19 +344,17 @@ export class Client {
212344
reject(e);
213345
}
214346

215-
const grpcCall = this.grpcClient.serverReflectionInfo(this.metadata);
216-
grpcCall.on('data', dataCallback);
217-
grpcCall.on('error', errorCallback);
218-
grpcCall.on('end', () => {
219-
resolve(result);
347+
const requests = symbolsToFetch.map(symbol => {
348+
const request = new this.CompatibleServerReflectionRequest!();
349+
return request.setFileByFilename(symbol);
220350
});
221351

222-
symbolsToFetch.forEach(symbol => {
223-
const request = new ServerReflectionRequest();
224-
grpcCall.write(request.setFileByFilename(symbol));
225-
});
226-
227-
grpcCall.end();
352+
this.sendReflectionRequest(requests)
353+
.then(responses => {
354+
for (const dataBit of responses) dataCallback(dataBit);
355+
resolve(result);
356+
})
357+
.catch(errorCallback);
228358
});
229359
}
230360
}

test/client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ListServiceResponse,
99
ServiceResponse,
1010
FileDescriptorResponse,
11-
} from '../src/reflection_pb';
11+
} from '../src/reflection_providers/v1/reflection_pb';
1212

1313
// eslint-disable-next-line no-undef
1414
describe('listServices', () => {

0 commit comments

Comments
 (0)