@zero-server/grpc 0.9.0 → 0.9.2

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.
@@ -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 };