@zero-server/sdk 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.
Files changed (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +460 -443
  3. package/index.js +414 -412
  4. package/lib/app.js +1172 -1172
  5. package/lib/auth/authorize.js +399 -399
  6. package/lib/auth/enrollment.js +367 -367
  7. package/lib/auth/index.js +57 -57
  8. package/lib/auth/jwt.js +731 -731
  9. package/lib/auth/oauth.js +362 -362
  10. package/lib/auth/session.js +588 -588
  11. package/lib/auth/trustedDevice.js +409 -409
  12. package/lib/auth/twoFactor.js +1150 -1150
  13. package/lib/auth/webauthn.js +946 -946
  14. package/lib/body/index.js +14 -14
  15. package/lib/body/json.js +109 -109
  16. package/lib/body/multipart.js +440 -440
  17. package/lib/body/raw.js +71 -71
  18. package/lib/body/rawBuffer.js +160 -160
  19. package/lib/body/sendError.js +25 -25
  20. package/lib/body/text.js +75 -75
  21. package/lib/body/typeMatch.js +41 -41
  22. package/lib/body/urlencoded.js +235 -235
  23. package/lib/cli.js +845 -845
  24. package/lib/cluster.js +666 -666
  25. package/lib/debug.js +372 -372
  26. package/lib/env/index.js +465 -465
  27. package/lib/errors.js +683 -683
  28. package/lib/fetch/index.js +256 -256
  29. package/lib/grpc/balancer.js +378 -378
  30. package/lib/grpc/call.js +708 -708
  31. package/lib/grpc/client.js +764 -764
  32. package/lib/grpc/codec.js +1221 -1221
  33. package/lib/grpc/credentials.js +398 -398
  34. package/lib/grpc/frame.js +262 -262
  35. package/lib/grpc/health.js +287 -287
  36. package/lib/grpc/index.js +121 -121
  37. package/lib/grpc/metadata.js +461 -461
  38. package/lib/grpc/proto.js +821 -821
  39. package/lib/grpc/reflection.js +590 -590
  40. package/lib/grpc/server.js +445 -445
  41. package/lib/grpc/status.js +118 -118
  42. package/lib/grpc/watch.js +173 -173
  43. package/lib/http/index.js +10 -10
  44. package/lib/http/request.js +727 -727
  45. package/lib/http/response.js +799 -799
  46. package/lib/lifecycle.js +557 -557
  47. package/lib/middleware/compress.js +230 -230
  48. package/lib/middleware/cookieParser.js +237 -237
  49. package/lib/middleware/cors.js +93 -93
  50. package/lib/middleware/csrf.js +137 -137
  51. package/lib/middleware/errorHandler.js +101 -101
  52. package/lib/middleware/helmet.js +175 -175
  53. package/lib/middleware/index.js +19 -17
  54. package/lib/middleware/logger.js +74 -74
  55. package/lib/middleware/rateLimit.js +88 -88
  56. package/lib/middleware/requestId.js +53 -53
  57. package/lib/middleware/static.js +326 -326
  58. package/lib/middleware/timeout.js +71 -71
  59. package/lib/middleware/validator.js +255 -255
  60. package/lib/observe/health.js +326 -326
  61. package/lib/observe/index.js +50 -50
  62. package/lib/observe/logger.js +359 -359
  63. package/lib/observe/metrics.js +805 -805
  64. package/lib/observe/tracing.js +592 -592
  65. package/lib/orm/adapters/json.js +290 -290
  66. package/lib/orm/adapters/memory.js +764 -764
  67. package/lib/orm/adapters/mongo.js +764 -764
  68. package/lib/orm/adapters/mysql.js +933 -933
  69. package/lib/orm/adapters/postgres.js +1144 -1144
  70. package/lib/orm/adapters/redis.js +1534 -1534
  71. package/lib/orm/adapters/sql-base.js +212 -212
  72. package/lib/orm/adapters/sqlite.js +858 -858
  73. package/lib/orm/audit.js +649 -649
  74. package/lib/orm/cache.js +394 -394
  75. package/lib/orm/geo.js +387 -387
  76. package/lib/orm/index.js +784 -784
  77. package/lib/orm/migrate.js +432 -432
  78. package/lib/orm/model.js +1706 -1706
  79. package/lib/orm/plugin.js +375 -375
  80. package/lib/orm/procedures.js +836 -836
  81. package/lib/orm/profiler.js +233 -233
  82. package/lib/orm/query.js +1772 -1772
  83. package/lib/orm/replicas.js +241 -241
  84. package/lib/orm/schema.js +307 -307
  85. package/lib/orm/search.js +380 -380
  86. package/lib/orm/seed/data/commerce.js +136 -136
  87. package/lib/orm/seed/data/internet.js +111 -111
  88. package/lib/orm/seed/data/locations.js +204 -204
  89. package/lib/orm/seed/data/names.js +338 -338
  90. package/lib/orm/seed/data/person.js +128 -128
  91. package/lib/orm/seed/data/phone.js +211 -211
  92. package/lib/orm/seed/data/words.js +134 -134
  93. package/lib/orm/seed/factory.js +178 -178
  94. package/lib/orm/seed/fake.js +1186 -1186
  95. package/lib/orm/seed/index.js +18 -18
  96. package/lib/orm/seed/rng.js +70 -70
  97. package/lib/orm/seed/seeder.js +124 -124
  98. package/lib/orm/seed/unique.js +68 -68
  99. package/lib/orm/snapshot.js +366 -366
  100. package/lib/orm/tenancy.js +605 -605
  101. package/lib/orm/views.js +350 -350
  102. package/lib/router/index.js +436 -436
  103. package/lib/sse/index.js +8 -8
  104. package/lib/sse/stream.js +349 -349
  105. package/lib/ws/connection.js +451 -451
  106. package/lib/ws/handshake.js +125 -125
  107. package/lib/ws/index.js +14 -14
  108. package/lib/ws/room.js +223 -223
  109. package/package.json +73 -73
  110. package/types/app.d.ts +223 -223
  111. package/types/auth.d.ts +520 -520
  112. package/types/body.d.ts +14 -0
  113. package/types/cli.d.ts +2 -0
  114. package/types/cluster.d.ts +75 -75
  115. package/types/env.d.ts +80 -80
  116. package/types/errors.d.ts +316 -316
  117. package/types/fetch.d.ts +43 -43
  118. package/types/grpc.d.ts +432 -432
  119. package/types/index.d.ts +384 -384
  120. package/types/lifecycle.d.ts +60 -60
  121. package/types/middleware.d.ts +320 -320
  122. package/types/observe.d.ts +304 -304
  123. package/types/orm.d.ts +1887 -1887
  124. package/types/request.d.ts +109 -109
  125. package/types/response.d.ts +157 -157
  126. package/types/router.d.ts +78 -78
  127. package/types/sse.d.ts +78 -78
  128. package/types/websocket.d.ts +126 -126
@@ -1,764 +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 };
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 };