@zero-server/grpc 0.9.1 → 0.9.3
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/LICENSE +21 -21
- package/index.d.ts +1 -1
- package/index.js +27 -27
- package/lib/debug.js +372 -0
- package/lib/grpc/balancer.js +378 -0
- package/lib/grpc/call.js +708 -0
- package/lib/grpc/client.js +764 -0
- package/lib/grpc/codec.js +1221 -0
- package/lib/grpc/credentials.js +398 -0
- package/lib/grpc/frame.js +262 -0
- package/lib/grpc/health.js +287 -0
- package/lib/grpc/index.js +121 -0
- package/lib/grpc/metadata.js +461 -0
- package/lib/grpc/proto.js +821 -0
- package/lib/grpc/reflection.js +590 -0
- package/lib/grpc/server.js +445 -0
- package/lib/grpc/status.js +118 -0
- package/lib/grpc/watch.js +173 -0
- package/package.json +10 -3
- package/types/app.d.ts +223 -0
- package/types/auth.d.ts +520 -0
- package/types/body.d.ts +14 -0
- package/types/cli.d.ts +2 -0
- package/types/cluster.d.ts +75 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +316 -0
- package/types/fetch.d.ts +43 -0
- package/types/grpc.d.ts +432 -0
- package/types/index.d.ts +384 -0
- package/types/lifecycle.d.ts +60 -0
- package/types/middleware.d.ts +320 -0
- package/types/observe.d.ts +304 -0
- package/types/orm.d.ts +1887 -0
- package/types/request.d.ts +109 -0
- package/types/response.d.ts +157 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +126 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module grpc/client
|
|
3
|
+
* @description Zero-dependency gRPC client using Node.js `http2.connect()`.
|
|
4
|
+
* Supports all four call types (unary, server-streaming,
|
|
5
|
+
* client-streaming, bidirectional), metadata, deadlines,
|
|
6
|
+
* automatic reconnection, and keep-alive.
|
|
7
|
+
*
|
|
8
|
+
* @example | Unary call
|
|
9
|
+
* const { GrpcClient, parseProto } = require('@zero-server/sdk');
|
|
10
|
+
* const schema = parseProto(fs.readFileSync('hello.proto', 'utf8'));
|
|
11
|
+
*
|
|
12
|
+
* const client = new GrpcClient('http://localhost:50051', schema, 'Greeter');
|
|
13
|
+
* const reply = await client.call('SayHello', { name: 'World' });
|
|
14
|
+
* console.log(reply.message); // => 'Hello World'
|
|
15
|
+
* client.close();
|
|
16
|
+
*
|
|
17
|
+
* @example | Server streaming
|
|
18
|
+
* const stream = client.serverStream('ListUsers', { pageSize: 10 });
|
|
19
|
+
* for await (const user of stream) {
|
|
20
|
+
* console.log(user.name);
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example | Bidirectional streaming
|
|
24
|
+
* const bidi = client.bidiStream('Chat');
|
|
25
|
+
* bidi.write({ text: 'hello' });
|
|
26
|
+
* for await (const reply of bidi) {
|
|
27
|
+
* console.log(reply.text);
|
|
28
|
+
* }
|
|
29
|
+
* bidi.end();
|
|
30
|
+
*
|
|
31
|
+
* @example | With TLS and metadata
|
|
32
|
+
* const client = new GrpcClient('https://api.example.com:443', schema, 'MyService', {
|
|
33
|
+
* ca: fs.readFileSync('ca.pem'),
|
|
34
|
+
* metadata: { authorization: 'Bearer <token>' },
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const http2 = require('http2');
|
|
39
|
+
const { EventEmitter } = require('events');
|
|
40
|
+
const log = require('../debug')('zero:grpc');
|
|
41
|
+
const { GrpcStatus, statusName } = require('./status');
|
|
42
|
+
const { Metadata } = require('./metadata');
|
|
43
|
+
const { frameEncode, FrameParser } = require('./frame');
|
|
44
|
+
const { encode, decode } = require('./codec');
|
|
45
|
+
|
|
46
|
+
// -- Client ------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* gRPC client for making RPC calls to a gRPC server.
|
|
50
|
+
* Supports single-address and multi-address (load-balanced) modes.
|
|
51
|
+
*
|
|
52
|
+
* @class
|
|
53
|
+
* @extends EventEmitter
|
|
54
|
+
*
|
|
55
|
+
* @param {string|object} address - Server address (e.g. `http://localhost:50051`) or options object.
|
|
56
|
+
* @param {string[]} [address.addresses] - Multiple backend addresses for load balancing.
|
|
57
|
+
* @param {string} [address.address] - Single backend address (alternative to string form).
|
|
58
|
+
* @param {string} [address.loadBalancing='pick-first'] - Load balancing policy ('pick-first' or 'round-robin').
|
|
59
|
+
* @param {boolean} [address.healthCheck=false] - Enable health-aware balancing.
|
|
60
|
+
* @param {object} schema - Parsed proto schema from `parseProto()`.
|
|
61
|
+
* @param {string} serviceName - Name of the service to call.
|
|
62
|
+
* @param {object} [opts] - Client options.
|
|
63
|
+
* @param {Buffer|string} [opts.ca] - CA certificate for TLS.
|
|
64
|
+
* @param {Buffer|string} [opts.key] - Client key for mTLS.
|
|
65
|
+
* @param {Buffer|string} [opts.cert] - Client certificate for mTLS.
|
|
66
|
+
* @param {object} [opts.metadata] - Default metadata sent with every call.
|
|
67
|
+
* @param {number} [opts.maxMessageSize=16777216] - Max incoming message size (16 MB default).
|
|
68
|
+
* @param {boolean} [opts.compress=false] - Compress outgoing messages.
|
|
69
|
+
* @param {number} [opts.deadline] - Default deadline in ms for all calls.
|
|
70
|
+
* @param {boolean} [opts.keepAlive=true] - Send HTTP/2 pings to keep connection alive.
|
|
71
|
+
* @param {number} [opts.keepAliveInterval=15000] - Ping interval in ms.
|
|
72
|
+
*/
|
|
73
|
+
class GrpcClient extends EventEmitter
|
|
74
|
+
{
|
|
75
|
+
constructor(address, schema, serviceName, opts = {})
|
|
76
|
+
{
|
|
77
|
+
super();
|
|
78
|
+
|
|
79
|
+
const service = schema.services[serviceName];
|
|
80
|
+
if (!service)
|
|
81
|
+
{
|
|
82
|
+
throw new Error(`Service "${serviceName}" not found in schema. ` +
|
|
83
|
+
`Available: ${Object.keys(schema.services).join(', ') || 'none'}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Support new multi-address API: new GrpcClient({ addresses: [...] }, schema, service, opts)
|
|
87
|
+
if (typeof address === 'object' && address !== null && !Buffer.isBuffer(address))
|
|
88
|
+
{
|
|
89
|
+
/** @private */
|
|
90
|
+
this._address = address.address || (address.addresses && address.addresses[0]) || '';
|
|
91
|
+
/** @private */
|
|
92
|
+
this._multiAddress = true;
|
|
93
|
+
|
|
94
|
+
// Merge address-level options into opts
|
|
95
|
+
if (address.ca) opts.ca = address.ca;
|
|
96
|
+
if (address.key) opts.key = address.key;
|
|
97
|
+
if (address.cert) opts.cert = address.cert;
|
|
98
|
+
if (address.metadata) opts.metadata = address.metadata;
|
|
99
|
+
if (address.rejectUnauthorized === false) opts.rejectUnauthorized = false;
|
|
100
|
+
|
|
101
|
+
// Create load balancer
|
|
102
|
+
if (address.addresses && address.addresses.length > 1)
|
|
103
|
+
{
|
|
104
|
+
const { LoadBalancer } = require('./balancer');
|
|
105
|
+
const connectOpts = {};
|
|
106
|
+
if (opts.ca) connectOpts.ca = opts.ca;
|
|
107
|
+
if (opts.key) connectOpts.key = opts.key;
|
|
108
|
+
if (opts.cert) connectOpts.cert = opts.cert;
|
|
109
|
+
if (opts.rejectUnauthorized === false) connectOpts.rejectUnauthorized = false;
|
|
110
|
+
|
|
111
|
+
/** @private */
|
|
112
|
+
this._balancer = new LoadBalancer(address.addresses, {
|
|
113
|
+
policy: address.loadBalancing || 'pick-first',
|
|
114
|
+
connectOpts,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else
|
|
118
|
+
{
|
|
119
|
+
this._balancer = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
{
|
|
124
|
+
/** @private */
|
|
125
|
+
this._address = address;
|
|
126
|
+
/** @private */
|
|
127
|
+
this._multiAddress = false;
|
|
128
|
+
/** @private */
|
|
129
|
+
this._balancer = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @private */
|
|
133
|
+
this._schema = schema;
|
|
134
|
+
/** @private */
|
|
135
|
+
this._service = service;
|
|
136
|
+
/** @private */
|
|
137
|
+
this._serviceName = serviceName;
|
|
138
|
+
/** @private */
|
|
139
|
+
this._opts = opts;
|
|
140
|
+
/** @private */
|
|
141
|
+
this._session = null;
|
|
142
|
+
/** @private */
|
|
143
|
+
this._closed = false;
|
|
144
|
+
/** @private */
|
|
145
|
+
this._keepAliveTimer = null;
|
|
146
|
+
|
|
147
|
+
// Build the path prefix (e.g. /mypackage.MyService)
|
|
148
|
+
const pkg = schema.package ? schema.package + '.' : '';
|
|
149
|
+
/** @private */
|
|
150
|
+
this._pathPrefix = '/' + pkg + serviceName;
|
|
151
|
+
|
|
152
|
+
/** Default metadata for all calls. */
|
|
153
|
+
this.defaultMetadata = new Metadata();
|
|
154
|
+
if (opts.metadata)
|
|
155
|
+
{
|
|
156
|
+
for (const [k, v] of Object.entries(opts.metadata))
|
|
157
|
+
this.defaultMetadata.set(k, v);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Lazily connect to the server. Reuses the HTTP/2 session if already connected.
|
|
163
|
+
* Uses the load balancer if configured for multi-address mode.
|
|
164
|
+
* @private
|
|
165
|
+
* @returns {import('http2').ClientHttp2Session}
|
|
166
|
+
*/
|
|
167
|
+
_connect()
|
|
168
|
+
{
|
|
169
|
+
// Load-balanced mode
|
|
170
|
+
if (this._balancer)
|
|
171
|
+
{
|
|
172
|
+
const session = this._balancer.getSession();
|
|
173
|
+
if (session) return session;
|
|
174
|
+
// Fallback to direct connect if balancer returns null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (this._session && !this._session.closed && !this._session.destroyed)
|
|
178
|
+
return this._session;
|
|
179
|
+
|
|
180
|
+
const connectOpts = {};
|
|
181
|
+
if (this._opts.ca) connectOpts.ca = this._opts.ca;
|
|
182
|
+
if (this._opts.key) connectOpts.key = this._opts.key;
|
|
183
|
+
if (this._opts.cert) connectOpts.cert = this._opts.cert;
|
|
184
|
+
// Allow self-signed certs in development
|
|
185
|
+
if (this._opts.rejectUnauthorized === false)
|
|
186
|
+
connectOpts.rejectUnauthorized = false;
|
|
187
|
+
|
|
188
|
+
this._session = http2.connect(this._address, connectOpts);
|
|
189
|
+
|
|
190
|
+
this._session.on('error', (err) =>
|
|
191
|
+
{
|
|
192
|
+
log.error('gRPC client session error: %s', err.message);
|
|
193
|
+
this.emit('error', err);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this._session.on('close', () =>
|
|
197
|
+
{
|
|
198
|
+
log.debug('gRPC client session closed');
|
|
199
|
+
this._session = null;
|
|
200
|
+
this.emit('disconnect');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Keep-alive pings
|
|
204
|
+
if (this._opts.keepAlive !== false)
|
|
205
|
+
{
|
|
206
|
+
const interval = this._opts.keepAliveInterval || 15000;
|
|
207
|
+
this._keepAliveTimer = setInterval(() =>
|
|
208
|
+
{
|
|
209
|
+
if (this._session && !this._session.closed)
|
|
210
|
+
{
|
|
211
|
+
this._session.ping((err) =>
|
|
212
|
+
{
|
|
213
|
+
if (err) log.debug('keep-alive ping failed: %s', err.message);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}, interval);
|
|
217
|
+
if (this._keepAliveTimer.unref) this._keepAliveTimer.unref();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
log.info('gRPC client connected to %s', this._address);
|
|
221
|
+
return this._session;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Build the HTTP/2 headers for a gRPC request.
|
|
226
|
+
* @private
|
|
227
|
+
* @param {string} methodName
|
|
228
|
+
* @param {Metadata|object} [extraMeta]
|
|
229
|
+
* @param {number} [deadline] - Deadline in ms.
|
|
230
|
+
* @returns {object}
|
|
231
|
+
*/
|
|
232
|
+
_buildHeaders(methodName, extraMeta, deadline)
|
|
233
|
+
{
|
|
234
|
+
const headers = {
|
|
235
|
+
':method': 'POST',
|
|
236
|
+
':path': this._pathPrefix + '/' + methodName,
|
|
237
|
+
'content-type': 'application/grpc+proto',
|
|
238
|
+
'te': 'trailers',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (this._opts.compress) headers['grpc-encoding'] = 'gzip';
|
|
242
|
+
|
|
243
|
+
// Merge metadata
|
|
244
|
+
const md = this.defaultMetadata.clone();
|
|
245
|
+
if (extraMeta)
|
|
246
|
+
{
|
|
247
|
+
const extra = extraMeta instanceof Metadata ? extraMeta : Metadata.fromHeaders(extraMeta);
|
|
248
|
+
md.merge(extra);
|
|
249
|
+
}
|
|
250
|
+
Object.assign(headers, md.toHeaders());
|
|
251
|
+
|
|
252
|
+
// Deadline -> grpc-timeout
|
|
253
|
+
const dl = deadline || this._opts.deadline;
|
|
254
|
+
if (dl)
|
|
255
|
+
{
|
|
256
|
+
// Convert ms to grpc-timeout format
|
|
257
|
+
if (dl >= 3600000) headers['grpc-timeout'] = Math.floor(dl / 3600000) + 'H';
|
|
258
|
+
else if (dl >= 60000) headers['grpc-timeout'] = Math.floor(dl / 60000) + 'M';
|
|
259
|
+
else if (dl >= 1000) headers['grpc-timeout'] = Math.floor(dl / 1000) + 'S';
|
|
260
|
+
else headers['grpc-timeout'] = dl + 'm';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return headers;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// -- Unary Call -----------------------------------------
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Make a unary gRPC call — send one message, receive one response.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} methodName - RPC method name as defined in the proto service.
|
|
272
|
+
* @param {object} request - Request message object.
|
|
273
|
+
* @param {object} [opts] - Call options.
|
|
274
|
+
* @param {Metadata|object} [opts.metadata] - Per-call metadata.
|
|
275
|
+
* @param {number} [opts.deadline] - Deadline in ms.
|
|
276
|
+
* @returns {Promise<object>} The decoded response message.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* const reply = await client.call('SayHello', { name: 'World' });
|
|
280
|
+
*/
|
|
281
|
+
call(methodName, request, opts = {})
|
|
282
|
+
{
|
|
283
|
+
const methodDef = this._service.methods[methodName];
|
|
284
|
+
if (!methodDef)
|
|
285
|
+
return Promise.reject(new Error(`Method "${methodName}" not found in service "${this._serviceName}"`));
|
|
286
|
+
|
|
287
|
+
const inputDesc = this._schema.messages[methodDef.inputType];
|
|
288
|
+
const outputDesc = this._schema.messages[methodDef.outputType];
|
|
289
|
+
if (!inputDesc) return Promise.reject(new Error(`Unknown input type: ${methodDef.inputType}`));
|
|
290
|
+
if (!outputDesc) return Promise.reject(new Error(`Unknown output type: ${methodDef.outputType}`));
|
|
291
|
+
|
|
292
|
+
return new Promise((resolve, reject) =>
|
|
293
|
+
{
|
|
294
|
+
const session = this._connect();
|
|
295
|
+
const headers = this._buildHeaders(methodName, opts.metadata, opts.deadline);
|
|
296
|
+
const stream = session.request(headers);
|
|
297
|
+
|
|
298
|
+
const parser = new FrameParser({ maxMessageSize: this._opts.maxMessageSize });
|
|
299
|
+
let response = null;
|
|
300
|
+
let grpcStatus = null;
|
|
301
|
+
let grpcMessage = null;
|
|
302
|
+
|
|
303
|
+
parser.onMessage = (buf) =>
|
|
304
|
+
{
|
|
305
|
+
try { response = decode(buf, outputDesc, this._schema.messages); }
|
|
306
|
+
catch (err) { reject(err); }
|
|
307
|
+
};
|
|
308
|
+
parser.onError = reject;
|
|
309
|
+
|
|
310
|
+
stream.on('data', (chunk) => parser.push(chunk));
|
|
311
|
+
|
|
312
|
+
// Trailers-Only response: grpc-status in initial headers
|
|
313
|
+
stream.on('response', (headers) =>
|
|
314
|
+
{
|
|
315
|
+
if (headers['grpc-status'] !== undefined)
|
|
316
|
+
{
|
|
317
|
+
grpcStatus = parseInt(headers['grpc-status'], 10);
|
|
318
|
+
grpcMessage = headers['grpc-message']
|
|
319
|
+
? decodeURIComponent(headers['grpc-message'])
|
|
320
|
+
: null;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
stream.on('trailers', (trailers) =>
|
|
325
|
+
{
|
|
326
|
+
grpcStatus = parseInt(trailers['grpc-status'] || '0', 10);
|
|
327
|
+
grpcMessage = trailers['grpc-message']
|
|
328
|
+
? decodeURIComponent(trailers['grpc-message'])
|
|
329
|
+
: null;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
stream.on('end', () =>
|
|
333
|
+
{
|
|
334
|
+
parser.destroy();
|
|
335
|
+
if (grpcStatus !== null && grpcStatus !== GrpcStatus.OK)
|
|
336
|
+
{
|
|
337
|
+
const err = new Error(grpcMessage || statusName(grpcStatus));
|
|
338
|
+
err.code = grpcStatus;
|
|
339
|
+
err.grpcCode = grpcStatus;
|
|
340
|
+
reject(err);
|
|
341
|
+
}
|
|
342
|
+
else
|
|
343
|
+
{
|
|
344
|
+
resolve(response || {});
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
stream.on('error', (err) =>
|
|
349
|
+
{
|
|
350
|
+
parser.destroy();
|
|
351
|
+
reject(err);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Deadline timeout
|
|
355
|
+
const dl = opts.deadline || this._opts.deadline;
|
|
356
|
+
if (dl)
|
|
357
|
+
{
|
|
358
|
+
const timer = setTimeout(() =>
|
|
359
|
+
{
|
|
360
|
+
stream.close();
|
|
361
|
+
const err = new Error('Deadline exceeded');
|
|
362
|
+
err.code = GrpcStatus.DEADLINE_EXCEEDED;
|
|
363
|
+
err.grpcCode = GrpcStatus.DEADLINE_EXCEEDED;
|
|
364
|
+
reject(err);
|
|
365
|
+
}, dl);
|
|
366
|
+
if (timer.unref) timer.unref();
|
|
367
|
+
stream.on('close', () => clearTimeout(timer));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Send the request
|
|
371
|
+
try
|
|
372
|
+
{
|
|
373
|
+
const buf = encode(request || {}, inputDesc, this._schema.messages);
|
|
374
|
+
const frame = frameEncode(buf, { compress: this._opts.compress });
|
|
375
|
+
|
|
376
|
+
if (frame instanceof Promise)
|
|
377
|
+
{
|
|
378
|
+
frame.then((f) =>
|
|
379
|
+
{
|
|
380
|
+
stream.write(f);
|
|
381
|
+
stream.end();
|
|
382
|
+
}).catch(reject);
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
{
|
|
386
|
+
stream.write(frame);
|
|
387
|
+
stream.end();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (err) { reject(err); }
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// -- Server Streaming ----------------------------------
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Make a server-streaming gRPC call — send one request, receive a stream of responses.
|
|
398
|
+
* Returns an async-iterable that yields decoded response messages.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} methodName - RPC method name.
|
|
401
|
+
* @param {object} request - Request message object.
|
|
402
|
+
* @param {object} [opts] - Call options.
|
|
403
|
+
* @param {Metadata|object} [opts.metadata] - Per-call metadata.
|
|
404
|
+
* @param {number} [opts.deadline] - Deadline in ms.
|
|
405
|
+
* @returns {AsyncIterable<object> & { cancel: Function }} Async iterable of response messages.
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* const stream = client.serverStream('ListUsers', { filter: 'active' });
|
|
409
|
+
* for await (const user of stream) {
|
|
410
|
+
* console.log(user);
|
|
411
|
+
* }
|
|
412
|
+
*/
|
|
413
|
+
serverStream(methodName, request, opts = {})
|
|
414
|
+
{
|
|
415
|
+
const methodDef = this._service.methods[methodName];
|
|
416
|
+
if (!methodDef) throw new Error(`Method "${methodName}" not found`);
|
|
417
|
+
|
|
418
|
+
const inputDesc = this._schema.messages[methodDef.inputType];
|
|
419
|
+
const outputDesc = this._schema.messages[methodDef.outputType];
|
|
420
|
+
|
|
421
|
+
const session = this._connect();
|
|
422
|
+
const headers = this._buildHeaders(methodName, opts.metadata, opts.deadline);
|
|
423
|
+
const stream = session.request(headers);
|
|
424
|
+
|
|
425
|
+
const parser = new FrameParser({ maxMessageSize: this._opts.maxMessageSize });
|
|
426
|
+
const queue = [];
|
|
427
|
+
let resolve = null;
|
|
428
|
+
let ended = false;
|
|
429
|
+
let error = null;
|
|
430
|
+
|
|
431
|
+
parser.onMessage = (buf) =>
|
|
432
|
+
{
|
|
433
|
+
try
|
|
434
|
+
{
|
|
435
|
+
const msg = decode(buf, outputDesc, this._schema.messages);
|
|
436
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: msg, done: false }); }
|
|
437
|
+
else queue.push(msg);
|
|
438
|
+
}
|
|
439
|
+
catch (err) { error = err; if (resolve) { const r = resolve; resolve = null; r(Promise.reject(err)); } }
|
|
440
|
+
};
|
|
441
|
+
parser.onError = (err) =>
|
|
442
|
+
{
|
|
443
|
+
error = err;
|
|
444
|
+
if (resolve) { const r = resolve; resolve = null; r(Promise.reject(err)); }
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
stream.on('data', (chunk) => parser.push(chunk));
|
|
448
|
+
|
|
449
|
+
// Trailers-Only response: grpc-status in initial headers
|
|
450
|
+
stream.on('response', (hdrs) =>
|
|
451
|
+
{
|
|
452
|
+
if (hdrs['grpc-status'] !== undefined)
|
|
453
|
+
{
|
|
454
|
+
const code = parseInt(hdrs['grpc-status'], 10);
|
|
455
|
+
if (code !== GrpcStatus.OK)
|
|
456
|
+
{
|
|
457
|
+
const msg = hdrs['grpc-message']
|
|
458
|
+
? decodeURIComponent(hdrs['grpc-message'])
|
|
459
|
+
: statusName(code);
|
|
460
|
+
error = new Error(msg);
|
|
461
|
+
error.code = code;
|
|
462
|
+
error.grpcCode = code;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
stream.on('end', () =>
|
|
468
|
+
{
|
|
469
|
+
ended = true;
|
|
470
|
+
parser.destroy();
|
|
471
|
+
if (error && resolve) { const r = resolve; resolve = null; r(Promise.reject(error)); }
|
|
472
|
+
else if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
|
|
473
|
+
});
|
|
474
|
+
stream.on('error', (err) =>
|
|
475
|
+
{
|
|
476
|
+
error = err;
|
|
477
|
+
ended = true;
|
|
478
|
+
parser.destroy();
|
|
479
|
+
if (resolve) { const r = resolve; resolve = null; r(Promise.reject(err)); }
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Send request
|
|
483
|
+
const buf = encode(request || {}, inputDesc, this._schema.messages);
|
|
484
|
+
const frame = frameEncode(buf, { compress: this._opts.compress });
|
|
485
|
+
if (frame instanceof Promise) frame.then((f) => { stream.write(f); stream.end(); });
|
|
486
|
+
else { stream.write(frame); stream.end(); }
|
|
487
|
+
|
|
488
|
+
const iterable = {
|
|
489
|
+
[Symbol.asyncIterator]()
|
|
490
|
+
{
|
|
491
|
+
return {
|
|
492
|
+
next()
|
|
493
|
+
{
|
|
494
|
+
if (error) return Promise.reject(error);
|
|
495
|
+
if (queue.length > 0)
|
|
496
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
497
|
+
if (ended)
|
|
498
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
499
|
+
return new Promise((r) => { resolve = r; });
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
cancel() { stream.close(); },
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
return iterable;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// -- Client Streaming ----------------------------------
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Make a client-streaming gRPC call — send a stream of requests, receive one response.
|
|
513
|
+
* Returns a writable object with `write()`, `end()`, and a `response` Promise.
|
|
514
|
+
*
|
|
515
|
+
* @param {string} methodName - RPC method name.
|
|
516
|
+
* @param {object} [opts] - Call options.
|
|
517
|
+
* @param {Metadata|object} [opts.metadata] - Per-call metadata.
|
|
518
|
+
* @param {number} [opts.deadline] - Deadline in ms.
|
|
519
|
+
* @returns {{ write: Function, end: Function, response: Promise<object> }}
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* const cs = client.clientStream('UploadChunks');
|
|
523
|
+
* cs.write({ data: chunk1 });
|
|
524
|
+
* cs.write({ data: chunk2 });
|
|
525
|
+
* cs.end();
|
|
526
|
+
* const result = await cs.response;
|
|
527
|
+
*/
|
|
528
|
+
clientStream(methodName, opts = {})
|
|
529
|
+
{
|
|
530
|
+
const methodDef = this._service.methods[methodName];
|
|
531
|
+
if (!methodDef) throw new Error(`Method "${methodName}" not found`);
|
|
532
|
+
|
|
533
|
+
const inputDesc = this._schema.messages[methodDef.inputType];
|
|
534
|
+
const outputDesc = this._schema.messages[methodDef.outputType];
|
|
535
|
+
|
|
536
|
+
const session = this._connect();
|
|
537
|
+
const headers = this._buildHeaders(methodName, opts.metadata, opts.deadline);
|
|
538
|
+
const stream = session.request(headers);
|
|
539
|
+
|
|
540
|
+
const parser = new FrameParser({ maxMessageSize: this._opts.maxMessageSize });
|
|
541
|
+
let responseMsg = null;
|
|
542
|
+
|
|
543
|
+
const response = new Promise((resolve, reject) =>
|
|
544
|
+
{
|
|
545
|
+
parser.onMessage = (buf) =>
|
|
546
|
+
{
|
|
547
|
+
try { responseMsg = decode(buf, outputDesc, this._schema.messages); }
|
|
548
|
+
catch (err) { reject(err); }
|
|
549
|
+
};
|
|
550
|
+
parser.onError = reject;
|
|
551
|
+
|
|
552
|
+
stream.on('data', (chunk) => parser.push(chunk));
|
|
553
|
+
|
|
554
|
+
// Trailers-Only response: grpc-status in initial headers
|
|
555
|
+
stream.on('response', (hdrs) =>
|
|
556
|
+
{
|
|
557
|
+
if (hdrs['grpc-status'] !== undefined)
|
|
558
|
+
{
|
|
559
|
+
const code = parseInt(hdrs['grpc-status'], 10);
|
|
560
|
+
if (code !== GrpcStatus.OK)
|
|
561
|
+
{
|
|
562
|
+
const msg = hdrs['grpc-message']
|
|
563
|
+
? decodeURIComponent(hdrs['grpc-message'])
|
|
564
|
+
: statusName(code);
|
|
565
|
+
const err = new Error(msg);
|
|
566
|
+
err.code = code;
|
|
567
|
+
err.grpcCode = code;
|
|
568
|
+
reject(err);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
stream.on('trailers', (trailers) =>
|
|
574
|
+
{
|
|
575
|
+
const code = parseInt(trailers['grpc-status'] || '0', 10);
|
|
576
|
+
if (code !== GrpcStatus.OK)
|
|
577
|
+
{
|
|
578
|
+
const msg = trailers['grpc-message']
|
|
579
|
+
? decodeURIComponent(trailers['grpc-message'])
|
|
580
|
+
: statusName(code);
|
|
581
|
+
const err = new Error(msg);
|
|
582
|
+
err.code = code;
|
|
583
|
+
err.grpcCode = code;
|
|
584
|
+
reject(err);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
stream.on('end', () => { parser.destroy(); resolve(responseMsg || {}); });
|
|
588
|
+
stream.on('error', (err) => { parser.destroy(); reject(err); });
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const compress = this._opts.compress;
|
|
592
|
+
const messages = this._schema.messages;
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
write(msg)
|
|
596
|
+
{
|
|
597
|
+
const buf = encode(msg, inputDesc, messages);
|
|
598
|
+
const frame = frameEncode(buf, { compress });
|
|
599
|
+
if (frame instanceof Promise) frame.then((f) => stream.write(f));
|
|
600
|
+
else stream.write(frame);
|
|
601
|
+
},
|
|
602
|
+
end() { stream.end(); },
|
|
603
|
+
cancel() { stream.close(); },
|
|
604
|
+
response,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// -- Bidirectional Streaming ----------------------------
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Make a bidirectional streaming gRPC call — send and receive streams simultaneously.
|
|
612
|
+
* Returns an object that is both writable (`write`/`end`) and async-iterable.
|
|
613
|
+
*
|
|
614
|
+
* @param {string} methodName - RPC method name.
|
|
615
|
+
* @param {object} [opts] - Call options.
|
|
616
|
+
* @param {Metadata|object} [opts.metadata] - Per-call metadata.
|
|
617
|
+
* @param {number} [opts.deadline] - Deadline in ms.
|
|
618
|
+
* @returns {AsyncIterable<object> & { write: Function, end: Function, cancel: Function }}
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* const bidi = client.bidiStream('Chat');
|
|
622
|
+
* bidi.write({ text: 'Hello' });
|
|
623
|
+
* for await (const reply of bidi) {
|
|
624
|
+
* console.log(reply.text);
|
|
625
|
+
* bidi.write({ text: 'got it' });
|
|
626
|
+
* }
|
|
627
|
+
* bidi.end();
|
|
628
|
+
*/
|
|
629
|
+
bidiStream(methodName, opts = {})
|
|
630
|
+
{
|
|
631
|
+
const methodDef = this._service.methods[methodName];
|
|
632
|
+
if (!methodDef) throw new Error(`Method "${methodName}" not found`);
|
|
633
|
+
|
|
634
|
+
const inputDesc = this._schema.messages[methodDef.inputType];
|
|
635
|
+
const outputDesc = this._schema.messages[methodDef.outputType];
|
|
636
|
+
|
|
637
|
+
const session = this._connect();
|
|
638
|
+
const headers = this._buildHeaders(methodName, opts.metadata, opts.deadline);
|
|
639
|
+
const stream = session.request(headers);
|
|
640
|
+
|
|
641
|
+
const parser = new FrameParser({ maxMessageSize: this._opts.maxMessageSize });
|
|
642
|
+
const queue = [];
|
|
643
|
+
let waitResolve = null;
|
|
644
|
+
let ended = false;
|
|
645
|
+
let error = null;
|
|
646
|
+
|
|
647
|
+
parser.onMessage = (buf) =>
|
|
648
|
+
{
|
|
649
|
+
try
|
|
650
|
+
{
|
|
651
|
+
const msg = decode(buf, outputDesc, this._schema.messages);
|
|
652
|
+
if (waitResolve) { const r = waitResolve; waitResolve = null; r({ value: msg, done: false }); }
|
|
653
|
+
else queue.push(msg);
|
|
654
|
+
}
|
|
655
|
+
catch (err) { error = err; if (waitResolve) { const r = waitResolve; waitResolve = null; r(Promise.reject(err)); } }
|
|
656
|
+
};
|
|
657
|
+
parser.onError = (err) =>
|
|
658
|
+
{
|
|
659
|
+
error = err;
|
|
660
|
+
if (waitResolve) { const r = waitResolve; waitResolve = null; r(Promise.reject(err)); }
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
stream.on('data', (chunk) => parser.push(chunk));
|
|
664
|
+
|
|
665
|
+
// Trailers-Only response: grpc-status in initial headers
|
|
666
|
+
stream.on('response', (hdrs) =>
|
|
667
|
+
{
|
|
668
|
+
if (hdrs['grpc-status'] !== undefined)
|
|
669
|
+
{
|
|
670
|
+
const code = parseInt(hdrs['grpc-status'], 10);
|
|
671
|
+
if (code !== GrpcStatus.OK)
|
|
672
|
+
{
|
|
673
|
+
const msg = hdrs['grpc-message']
|
|
674
|
+
? decodeURIComponent(hdrs['grpc-message'])
|
|
675
|
+
: statusName(code);
|
|
676
|
+
error = new Error(msg);
|
|
677
|
+
error.code = code;
|
|
678
|
+
error.grpcCode = code;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
stream.on('end', () =>
|
|
684
|
+
{
|
|
685
|
+
ended = true;
|
|
686
|
+
parser.destroy();
|
|
687
|
+
if (error && waitResolve) { const r = waitResolve; waitResolve = null; r(Promise.reject(error)); }
|
|
688
|
+
else if (waitResolve) { const r = waitResolve; waitResolve = null; r({ value: undefined, done: true }); }
|
|
689
|
+
});
|
|
690
|
+
stream.on('error', (err) =>
|
|
691
|
+
{
|
|
692
|
+
error = err;
|
|
693
|
+
ended = true;
|
|
694
|
+
parser.destroy();
|
|
695
|
+
if (waitResolve) { const r = waitResolve; waitResolve = null; r(Promise.reject(err)); }
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const compress = this._opts.compress;
|
|
699
|
+
const messages = this._schema.messages;
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
write(msg)
|
|
703
|
+
{
|
|
704
|
+
const buf = encode(msg, inputDesc, messages);
|
|
705
|
+
const frame = frameEncode(buf, { compress });
|
|
706
|
+
if (frame instanceof Promise) frame.then((f) => stream.write(f));
|
|
707
|
+
else stream.write(frame);
|
|
708
|
+
},
|
|
709
|
+
end() { stream.end(); },
|
|
710
|
+
cancel() { stream.close(); },
|
|
711
|
+
[Symbol.asyncIterator]()
|
|
712
|
+
{
|
|
713
|
+
return {
|
|
714
|
+
next()
|
|
715
|
+
{
|
|
716
|
+
if (error) return Promise.reject(error);
|
|
717
|
+
if (queue.length > 0)
|
|
718
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
719
|
+
if (ended)
|
|
720
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
721
|
+
return new Promise((r) => { waitResolve = r; });
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// -- Lifecycle ------------------------------------------
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Close the client connection.
|
|
732
|
+
*/
|
|
733
|
+
close()
|
|
734
|
+
{
|
|
735
|
+
this._closed = true;
|
|
736
|
+
if (this._keepAliveTimer)
|
|
737
|
+
{
|
|
738
|
+
clearInterval(this._keepAliveTimer);
|
|
739
|
+
this._keepAliveTimer = null;
|
|
740
|
+
}
|
|
741
|
+
if (this._balancer)
|
|
742
|
+
{
|
|
743
|
+
this._balancer.shutdown();
|
|
744
|
+
this._balancer = null;
|
|
745
|
+
}
|
|
746
|
+
if (this._session)
|
|
747
|
+
{
|
|
748
|
+
this._session.close();
|
|
749
|
+
this._session = null;
|
|
750
|
+
}
|
|
751
|
+
log.info('gRPC client closed');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Check if the client is connected.
|
|
756
|
+
* @returns {boolean}
|
|
757
|
+
*/
|
|
758
|
+
get connected()
|
|
759
|
+
{
|
|
760
|
+
return this._session && !this._session.closed && !this._session.destroyed;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
module.exports = { GrpcClient };
|