@zmdb/transport-grpc 1.0.0-beta.1

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,711 @@
1
+ import { once } from 'node:events';
2
+ import { Client, Metadata, Server, ServerCredentials, credentials, status, } from '@grpc/grpc-js';
3
+ import { GrpcError, } from './types.js';
4
+ class BoundGrpcService {
5
+ service;
6
+ methods;
7
+ #spec;
8
+ #handlers;
9
+ constructor(spec, handlers) {
10
+ this.#spec = spec;
11
+ this.#handlers = handlers;
12
+ this.service = spec.definition.name;
13
+ this.methods = Object.freeze(Object.keys(spec.definition.methods));
14
+ }
15
+ register(server) {
16
+ const definition = Object.create(null);
17
+ const implementation = Object.create(null);
18
+ for (const [name, method] of methodEntries(this.#spec.definition.methods)) {
19
+ definition[name] = {
20
+ path: method.path,
21
+ requestStream: method.requestStream,
22
+ responseStream: method.responseStream,
23
+ requestSerialize: value => grpcBytes(serializeRequest(method, value)),
24
+ requestDeserialize: bytes => decodeRequest(method, bytes),
25
+ responseSerialize: value => grpcBytes(serializeResponse(method, value)),
26
+ responseDeserialize: bytes => deserializeResponse(method, bytes),
27
+ };
28
+ implementation[name] = serverHandler(name, method, this.#spec, this.#handlers);
29
+ }
30
+ const serviceDefinition = definition;
31
+ server.addService(serviceDefinition, implementation);
32
+ }
33
+ }
34
+ /** Bind one generated service definition to its exhaustive typed handler map. */
35
+ export function bindGrpcService(service, handlers) {
36
+ validateServiceSpec(service);
37
+ return new BoundGrpcService(service, handlers);
38
+ }
39
+ /** Attach one gRPC server to the protocol-neutral application lifecycle. */
40
+ export function grpcExtension(options) {
41
+ let opened;
42
+ return {
43
+ name: '@zmdb/transport-grpc',
44
+ async start() {
45
+ opened = await openGrpcServer(options);
46
+ },
47
+ async stop({ graceMs }) {
48
+ try {
49
+ await opened?.close(graceMs);
50
+ }
51
+ finally {
52
+ opened = undefined;
53
+ }
54
+ },
55
+ };
56
+ }
57
+ /** Create a typed client from the same generated service artifact used by the server. */
58
+ export function createGrpcClient(options) {
59
+ validatePositiveDuration(options.deadlineMs, 'deadlineMs');
60
+ const channel = new Client(options.address, clientCredentials(options.credentials));
61
+ const client = Object.create(null);
62
+ for (const [name, method] of methodEntries(options.definition.methods)) {
63
+ if (name === 'close') {
64
+ channel.close();
65
+ throw new Error('@zmdb/transport-grpc: a gRPC method cannot be named "close" because the typed client owns that member');
66
+ }
67
+ Object.defineProperty(client, name, {
68
+ configurable: false,
69
+ enumerable: true,
70
+ value: clientCaller(channel, method, options),
71
+ writable: false,
72
+ });
73
+ }
74
+ const close = () => {
75
+ channel.close();
76
+ };
77
+ Object.defineProperties(client, {
78
+ close: { configurable: false, enumerable: false, value: close, writable: false },
79
+ [Symbol.dispose]: { configurable: false, enumerable: false, value: close, writable: false },
80
+ });
81
+ return client;
82
+ }
83
+ /** Start all bound services. Kept internal to the application lifecycle. */
84
+ export async function openGrpcServer(options) {
85
+ if (options.address.length === 0) {
86
+ throw new RangeError('@zmdb/transport-grpc: a gRPC server address cannot be empty');
87
+ }
88
+ if (options.bindings.length === 0) {
89
+ throw new RangeError('@zmdb/transport-grpc: a gRPC server requires at least one binding');
90
+ }
91
+ const server = new Server();
92
+ try {
93
+ for (const binding of options.bindings) {
94
+ if (!(binding instanceof BoundGrpcService)) {
95
+ throw new TypeError('@zmdb/transport-grpc: gRPC bindings must be created by bindGrpcService');
96
+ }
97
+ binding.register(server);
98
+ }
99
+ const port = await bindServer(server, options.address, serverCredentials(options.credentials));
100
+ let closePromise;
101
+ return {
102
+ port,
103
+ close: graceMs => {
104
+ closePromise ??= closeServer(server, graceMs);
105
+ return closePromise;
106
+ },
107
+ };
108
+ }
109
+ catch (error) {
110
+ server.forceShutdown();
111
+ throw error;
112
+ }
113
+ }
114
+ function methodEntries(methods) {
115
+ return Object.entries(methods);
116
+ }
117
+ function validateServiceSpec(service) {
118
+ if (service.definition.name.length === 0) {
119
+ throw new RangeError('@zmdb/transport-grpc: a gRPC service name cannot be empty');
120
+ }
121
+ if (Object.keys(service.definition.methods).length === 0) {
122
+ throw new RangeError(`@zmdb/transport-grpc: gRPC service "${service.definition.name}" has no methods`);
123
+ }
124
+ if (service.maxDurationMs !== undefined) {
125
+ validatePositiveDuration(service.maxDurationMs, 'maxDurationMs');
126
+ }
127
+ }
128
+ function validatePositiveDuration(value, name) {
129
+ if (!Number.isFinite(value) || value <= 0) {
130
+ throw new RangeError(`@zmdb/transport-grpc: ${name} must be a positive finite number`);
131
+ }
132
+ }
133
+ function grpcBytes(bytes) {
134
+ return globalThis.Buffer.from(bytes);
135
+ }
136
+ function decodeRequest(method, bytes) {
137
+ try {
138
+ const value = method.deserializeRequest(bytes);
139
+ return { ok: true, value: method.validateRequest(value) };
140
+ }
141
+ catch (error) {
142
+ return { ok: false, error };
143
+ }
144
+ }
145
+ function serializeRequest(method, value) {
146
+ return method.serializeRequest(method.validateRequest(value));
147
+ }
148
+ function serializeResponse(method, value) {
149
+ return method.serializeResponse(method.validateResponse(value));
150
+ }
151
+ function deserializeResponse(method, bytes) {
152
+ return method.validateResponse(method.deserializeResponse(bytes));
153
+ }
154
+ function serverHandler(name, method, spec, handlers) {
155
+ if (method.requestStream) {
156
+ return method.responseStream
157
+ ? (call) => {
158
+ void runBidi(call, name, method, spec, handlers);
159
+ }
160
+ : (call, callback) => {
161
+ void runClientStream(call, callback, name, method, spec, handlers);
162
+ };
163
+ }
164
+ return method.responseStream
165
+ ? (call) => {
166
+ void runServerStream(call, name, method, spec, handlers);
167
+ }
168
+ : (call, callback) => {
169
+ void runUnary(call, callback, name, method, spec, handlers);
170
+ };
171
+ }
172
+ async function runUnary(call, callback, name, method, spec, handlers) {
173
+ const scope = callScope(call, spec.maxDurationMs);
174
+ try {
175
+ const request = requestValue(call.request);
176
+ const context = grpcCall(call, spec, name, request, scope);
177
+ const handler = handlerAt(handlers, name);
178
+ const response = method.validateResponse(await handler(context));
179
+ callback(null, response, trailers(scope.trailers));
180
+ }
181
+ catch (error) {
182
+ callback(serverError(boundaryError(error, scope, spec, name)));
183
+ }
184
+ finally {
185
+ scope.close();
186
+ }
187
+ }
188
+ async function runClientStream(call, callback, name, method, spec, handlers) {
189
+ const scope = callScope(call, spec.maxDurationMs);
190
+ try {
191
+ const requests = requestStream(call, scope);
192
+ const context = grpcCall(call, spec, name, requests, scope);
193
+ const handler = handlerAt(handlers, name);
194
+ const response = method.validateResponse(await handler(context));
195
+ callback(null, response, trailers(scope.trailers));
196
+ }
197
+ catch (error) {
198
+ callback(serverError(boundaryError(error, scope, spec, name)));
199
+ }
200
+ finally {
201
+ scope.close();
202
+ }
203
+ }
204
+ async function runServerStream(call, name, method, spec, handlers) {
205
+ const scope = callScope(call, spec.maxDurationMs);
206
+ try {
207
+ const request = requestValue(call.request);
208
+ const context = grpcCall(call, spec, name, request, scope);
209
+ const handler = handlerAt(handlers, name);
210
+ await writeResponses(call, handler(context), method, scope);
211
+ call.end(trailers(scope.trailers));
212
+ }
213
+ catch (error) {
214
+ call.destroy(serverError(boundaryError(error, scope, spec, name)));
215
+ }
216
+ finally {
217
+ scope.close();
218
+ }
219
+ }
220
+ async function runBidi(call, name, method, spec, handlers) {
221
+ const scope = callScope(call, spec.maxDurationMs);
222
+ try {
223
+ const requests = requestStream(call, scope);
224
+ const context = grpcCall(call, spec, name, requests, scope);
225
+ const handler = handlerAt(handlers, name);
226
+ await writeResponses(call, handler(context), method, scope);
227
+ call.end(trailers(scope.trailers));
228
+ }
229
+ catch (error) {
230
+ call.destroy(serverError(boundaryError(error, scope, spec, name)));
231
+ }
232
+ finally {
233
+ scope.close();
234
+ }
235
+ }
236
+ function requestValue(decoded) {
237
+ if (!decoded.ok) {
238
+ throw new GrpcError('INVALID_ARGUMENT', 'invalid request');
239
+ }
240
+ return decoded.value;
241
+ }
242
+ async function* requestStream(call, scope) {
243
+ const iterator = call[Symbol.asyncIterator]();
244
+ try {
245
+ for (;;) {
246
+ const next = await nextRequest(iterator, scope);
247
+ if (next.done)
248
+ return;
249
+ yield requestValue(next.value);
250
+ }
251
+ }
252
+ finally {
253
+ await iterator.return?.();
254
+ }
255
+ }
256
+ async function nextRequest(iterator, scope) {
257
+ if (scope.signal.aborted)
258
+ throw scope.reason();
259
+ let removeAbort = () => undefined;
260
+ const aborted = new Promise((_resolve, reject) => {
261
+ const onAbort = () => {
262
+ reject(scope.reason());
263
+ };
264
+ scope.signal.addEventListener('abort', onAbort, { once: true });
265
+ removeAbort = () => {
266
+ scope.signal.removeEventListener('abort', onAbort);
267
+ };
268
+ });
269
+ try {
270
+ return await Promise.race([iterator.next(), aborted]);
271
+ }
272
+ finally {
273
+ removeAbort();
274
+ }
275
+ }
276
+ async function writeResponses(call, responses, method, scope) {
277
+ for await (const response of responses) {
278
+ if (scope.signal.aborted)
279
+ throw scope.reason();
280
+ const valid = method.validateResponse(response);
281
+ if (!call.write(valid)) {
282
+ await waitForDrain(call, scope);
283
+ }
284
+ }
285
+ }
286
+ function handlerAt(handlers, name) {
287
+ return Reflect.get(handlers, name);
288
+ }
289
+ function waitForDrain(call, scope) {
290
+ if (scope.signal.aborted)
291
+ return Promise.reject(scope.reason());
292
+ return new Promise((resolve, reject) => {
293
+ const onDrain = () => {
294
+ scope.signal.removeEventListener('abort', onAbort);
295
+ resolve();
296
+ };
297
+ const onAbort = () => {
298
+ call.removeListener('drain', onDrain);
299
+ reject(scope.reason());
300
+ };
301
+ call.once('drain', onDrain);
302
+ scope.signal.addEventListener('abort', onAbort, { once: true });
303
+ });
304
+ }
305
+ function grpcCall(call, spec, method, payload, scope) {
306
+ let metadata;
307
+ try {
308
+ metadata = validateMetadata(call.metadata, spec.validateMetadata);
309
+ }
310
+ catch {
311
+ throw new GrpcError('INVALID_ARGUMENT', 'invalid metadata');
312
+ }
313
+ return {
314
+ kind: 'grpc',
315
+ service: spec.definition.name,
316
+ method,
317
+ payload,
318
+ headers: metadata.headers,
319
+ binaryHeaders: metadata.binaryHeaders,
320
+ peer: call.getPeer(),
321
+ signal: scope.signal,
322
+ remainingMs: () => scope.remainingMs(),
323
+ setTrailer: (key, value) => {
324
+ scope.setTrailer(key, value);
325
+ },
326
+ };
327
+ }
328
+ function callScope(call, maxDurationMs) {
329
+ const controller = new AbortController();
330
+ const trailerValues = Object.create(null);
331
+ const now = Date.now();
332
+ const callerDeadline = deadlineTime(call.getDeadline());
333
+ const serverDeadline = maxDurationMs === undefined ? Number.POSITIVE_INFINITY : now + maxDurationMs;
334
+ const deadline = Math.min(callerDeadline, serverDeadline);
335
+ let timer;
336
+ const abort = (reason) => {
337
+ if (!controller.signal.aborted)
338
+ controller.abort(reason);
339
+ };
340
+ const onCancelled = () => {
341
+ const expired = Number.isFinite(deadline) && Date.now() >= deadline;
342
+ abort(expired ? new GrpcError('DEADLINE_EXCEEDED', 'deadline exceeded') : new GrpcError('CANCELLED', 'call cancelled'));
343
+ };
344
+ const scheduleDeadline = () => {
345
+ if (!Number.isFinite(deadline) || controller.signal.aborted)
346
+ return;
347
+ const remaining = deadline - Date.now();
348
+ if (remaining <= 0) {
349
+ abort(new GrpcError('DEADLINE_EXCEEDED', 'deadline exceeded'));
350
+ return;
351
+ }
352
+ timer = setTimeout(scheduleDeadline, Math.min(remaining, 2_147_483_647));
353
+ };
354
+ call.on('cancelled', onCancelled);
355
+ if (call.cancelled)
356
+ onCancelled();
357
+ scheduleDeadline();
358
+ return {
359
+ signal: controller.signal,
360
+ trailers: trailerValues,
361
+ remainingMs: () => {
362
+ if (controller.signal.aborted)
363
+ return 0;
364
+ return Number.isFinite(deadline) ? Math.max(0, deadline - Date.now()) : Number.POSITIVE_INFINITY;
365
+ },
366
+ setTrailer: (key, value) => {
367
+ const probe = new Metadata();
368
+ probe.set(key, value);
369
+ trailerValues[key] = value;
370
+ },
371
+ reason: () => controller.signal.reason instanceof GrpcError
372
+ ? controller.signal.reason
373
+ : new GrpcError('CANCELLED', 'call cancelled'),
374
+ close: () => {
375
+ if (timer !== undefined)
376
+ clearTimeout(timer);
377
+ call.removeListener('cancelled', onCancelled);
378
+ },
379
+ };
380
+ }
381
+ function deadlineTime(deadline) {
382
+ return deadline instanceof Date ? deadline.getTime() : deadline;
383
+ }
384
+ function validateMetadata(metadata, validate) {
385
+ return validate(metadataValue(metadata));
386
+ }
387
+ function metadataValue(metadata) {
388
+ const headers = Object.create(null);
389
+ const binaryHeaders = Object.create(null);
390
+ for (const [key, values] of Object.entries(metadata.toJSON())) {
391
+ const value = values[0];
392
+ if (typeof value === 'string') {
393
+ headers[key] = value;
394
+ }
395
+ else if (value !== undefined) {
396
+ binaryHeaders[key] = Uint8Array.from(value);
397
+ }
398
+ }
399
+ return { headers, binaryHeaders };
400
+ }
401
+ function outboundMetadata(metadata) {
402
+ const result = new Metadata();
403
+ if (metadata === undefined)
404
+ return result;
405
+ for (const [key, value] of Object.entries(metadata.headers))
406
+ result.set(key, value);
407
+ for (const [key, value] of Object.entries(metadata.binaryHeaders))
408
+ result.set(key, grpcBytes(value));
409
+ return result;
410
+ }
411
+ function trailers(values) {
412
+ const result = new Metadata();
413
+ for (const [key, value] of Object.entries(values))
414
+ result.set(key, value);
415
+ return result;
416
+ }
417
+ function boundaryError(error, scope, spec, method) {
418
+ if (error instanceof GrpcError)
419
+ return error;
420
+ if (scope.signal.aborted)
421
+ return scope.reason();
422
+ try {
423
+ spec.onError({
424
+ service: spec.definition.name,
425
+ method,
426
+ status: 'INTERNAL',
427
+ error,
428
+ });
429
+ }
430
+ catch {
431
+ // Observation must not replace the fixed boundary error.
432
+ }
433
+ return new GrpcError('INTERNAL', 'internal error');
434
+ }
435
+ function serverError(error) {
436
+ return Object.assign(new Error(error.details), {
437
+ code: statusCode(error.status),
438
+ details: error.details,
439
+ });
440
+ }
441
+ function statusCode(value) {
442
+ switch (value) {
443
+ case 'OK':
444
+ return status.OK;
445
+ case 'CANCELLED':
446
+ return status.CANCELLED;
447
+ case 'INVALID_ARGUMENT':
448
+ return status.INVALID_ARGUMENT;
449
+ case 'DEADLINE_EXCEEDED':
450
+ return status.DEADLINE_EXCEEDED;
451
+ case 'NOT_FOUND':
452
+ return status.NOT_FOUND;
453
+ case 'ALREADY_EXISTS':
454
+ return status.ALREADY_EXISTS;
455
+ case 'PERMISSION_DENIED':
456
+ return status.PERMISSION_DENIED;
457
+ case 'RESOURCE_EXHAUSTED':
458
+ return status.RESOURCE_EXHAUSTED;
459
+ case 'FAILED_PRECONDITION':
460
+ return status.FAILED_PRECONDITION;
461
+ case 'UNIMPLEMENTED':
462
+ return status.UNIMPLEMENTED;
463
+ case 'INTERNAL':
464
+ return status.INTERNAL;
465
+ case 'UNAVAILABLE':
466
+ return status.UNAVAILABLE;
467
+ case 'UNAUTHENTICATED':
468
+ return status.UNAUTHENTICATED;
469
+ }
470
+ }
471
+ function serverCredentials(options) {
472
+ if (options === 'insecure')
473
+ return ServerCredentials.createInsecure();
474
+ return ServerCredentials.createSsl(options.rootCertificates === undefined ? null : grpcBytes(options.rootCertificates), options.keyCertPairs.map(pair => ({
475
+ private_key: grpcBytes(pair.privateKey),
476
+ cert_chain: grpcBytes(pair.certificateChain),
477
+ })), options.checkClientCertificate);
478
+ }
479
+ function clientCredentials(options) {
480
+ if (options === 'insecure')
481
+ return credentials.createInsecure();
482
+ const hasPrivateKey = options.privateKey !== undefined;
483
+ const hasCertificate = options.certificateChain !== undefined;
484
+ if (hasPrivateKey !== hasCertificate) {
485
+ throw new Error('@zmdb/transport-grpc: gRPC client privateKey and certificateChain must be supplied together');
486
+ }
487
+ return credentials.createSsl(options.rootCertificates === undefined ? null : grpcBytes(options.rootCertificates), options.privateKey === undefined ? null : grpcBytes(options.privateKey), options.certificateChain === undefined ? null : grpcBytes(options.certificateChain));
488
+ }
489
+ function bindServer(server, address, creds) {
490
+ return new Promise((resolve, reject) => {
491
+ server.bindAsync(address, creds, (error, port) => {
492
+ if (error === null)
493
+ resolve(port);
494
+ else
495
+ reject(error);
496
+ });
497
+ });
498
+ }
499
+ function closeServer(server, graceMs) {
500
+ if (!Number.isFinite(graceMs) || graceMs < 0) {
501
+ throw new RangeError('@zmdb/transport-grpc: graceMs must be a non-negative finite number');
502
+ }
503
+ if (graceMs === 0) {
504
+ server.forceShutdown();
505
+ return Promise.resolve();
506
+ }
507
+ return new Promise((resolve, reject) => {
508
+ let settled = false;
509
+ const timer = setTimeout(() => {
510
+ if (settled)
511
+ return;
512
+ settled = true;
513
+ server.forceShutdown();
514
+ resolve();
515
+ }, graceMs);
516
+ server.tryShutdown(error => {
517
+ if (settled)
518
+ return;
519
+ settled = true;
520
+ clearTimeout(timer);
521
+ if (error === undefined)
522
+ resolve();
523
+ else
524
+ reject(error);
525
+ });
526
+ });
527
+ }
528
+ function clientCaller(client, method, options) {
529
+ if (method.requestStream) {
530
+ return method.responseStream
531
+ ? (payload, callOptions) => bidiCall(client, method, payload, options, callOptions)
532
+ : (payload, callOptions) => clientStreamCall(client, method, payload, options, callOptions);
533
+ }
534
+ return method.responseStream
535
+ ? (payload, callOptions) => serverStreamCall(client, method, payload, options, callOptions)
536
+ : (payload, callOptions) => unaryCall(client, method, payload, options, callOptions);
537
+ }
538
+ function unaryCall(client, method, payload, options, callOptions) {
539
+ const request = method.validateRequest(payload);
540
+ const deadlineMs = callDeadline(options.deadlineMs, callOptions);
541
+ return new Promise((resolve, reject) => {
542
+ let observation;
543
+ let removeAbort = () => undefined;
544
+ const call = client.makeUnaryRequest(method.path, value => grpcBytes(method.serializeRequest(method.validateRequest(value))), bytes => method.validateResponse(method.deserializeResponse(bytes)), request, outboundMetadata(callOptions?.metadata), { deadline: Date.now() + deadlineMs }, (error, response) => {
545
+ removeAbort();
546
+ if (error !== null) {
547
+ reject(observation?.error ?? error);
548
+ }
549
+ else {
550
+ resolve(response);
551
+ }
552
+ });
553
+ observation = new ClientObservation(call, options.validateMetadata, callOptions);
554
+ removeAbort = attachAbort(call, callOptions?.signal);
555
+ });
556
+ }
557
+ function clientStreamCall(client, method, payload, options, callOptions) {
558
+ const deadlineMs = callDeadline(options.deadlineMs, callOptions);
559
+ return new Promise((resolve, reject) => {
560
+ let settled = false;
561
+ let observation;
562
+ let removeAbort = () => undefined;
563
+ const call = client.makeClientStreamRequest(method.path, value => grpcBytes(method.serializeRequest(method.validateRequest(value))), bytes => method.validateResponse(method.deserializeResponse(bytes)), outboundMetadata(callOptions?.metadata), { deadline: Date.now() + deadlineMs }, (error, response) => {
564
+ if (settled)
565
+ return;
566
+ settled = true;
567
+ removeAbort();
568
+ if (error !== null)
569
+ reject(observation?.error ?? error);
570
+ else
571
+ resolve(response);
572
+ });
573
+ observation = new ClientObservation(call, options.validateMetadata, callOptions);
574
+ removeAbort = attachAbort(call, callOptions?.signal);
575
+ void pumpRequests(call, payload, method).catch(error => {
576
+ if (settled)
577
+ return;
578
+ settled = true;
579
+ removeAbort();
580
+ call.cancel();
581
+ reject(error);
582
+ });
583
+ });
584
+ }
585
+ function serverStreamCall(client, method, payload, options, callOptions) {
586
+ const request = method.validateRequest(payload);
587
+ const deadlineMs = callDeadline(options.deadlineMs, callOptions);
588
+ const call = client.makeServerStreamRequest(method.path, value => grpcBytes(method.serializeRequest(method.validateRequest(value))), bytes => method.validateResponse(method.deserializeResponse(bytes)), request, outboundMetadata(callOptions?.metadata), { deadline: Date.now() + deadlineMs });
589
+ const observation = new ClientObservation(call, options.validateMetadata, callOptions);
590
+ const removeAbort = attachAbort(call, callOptions?.signal);
591
+ return clientResponses(call, observation, removeAbort);
592
+ }
593
+ function bidiCall(client, method, payload, options, callOptions) {
594
+ const deadlineMs = callDeadline(options.deadlineMs, callOptions);
595
+ const call = client.makeBidiStreamRequest(method.path, value => grpcBytes(method.serializeRequest(method.validateRequest(value))), bytes => method.validateResponse(method.deserializeResponse(bytes)), outboundMetadata(callOptions?.metadata), { deadline: Date.now() + deadlineMs });
596
+ const observation = new ClientObservation(call, options.validateMetadata, callOptions);
597
+ const removeAbort = attachAbort(call, callOptions?.signal);
598
+ const pumping = requestPump(call, payload, method);
599
+ return clientResponses(call, observation, removeAbort, pumping);
600
+ }
601
+ function callDeadline(defaultMs, options) {
602
+ const value = options?.deadlineMs ?? defaultMs;
603
+ validatePositiveDuration(value, 'deadlineMs');
604
+ return value;
605
+ }
606
+ async function pumpRequests(call, requests, method) {
607
+ for await (const request of requests) {
608
+ const valid = method.validateRequest(request);
609
+ if (!call.write(valid))
610
+ await once(call, 'drain');
611
+ }
612
+ call.end();
613
+ }
614
+ function requestPump(call, requests, method) {
615
+ let failure = { failed: false };
616
+ const done = pumpRequests(call, requests, method).catch(error => {
617
+ failure = { failed: true, error };
618
+ call.cancel();
619
+ });
620
+ return {
621
+ done,
622
+ failure: () => failure,
623
+ };
624
+ }
625
+ async function* clientResponses(call, observation, removeAbort, pumping) {
626
+ let completed = false;
627
+ try {
628
+ for await (const response of call) {
629
+ observation.throwIfInvalid();
630
+ yield response;
631
+ }
632
+ if (pumping !== undefined) {
633
+ await pumping.done;
634
+ throwPumpFailure(pumping);
635
+ }
636
+ await observation.finished;
637
+ observation.throwIfInvalid();
638
+ completed = true;
639
+ }
640
+ catch (error) {
641
+ observation.throwIfInvalid();
642
+ if (pumping !== undefined)
643
+ throwPumpFailure(pumping);
644
+ throw error;
645
+ }
646
+ finally {
647
+ removeAbort();
648
+ if (!completed)
649
+ call.cancel();
650
+ }
651
+ }
652
+ function throwPumpFailure(pumping) {
653
+ const failure = pumping.failure();
654
+ if (failure.failed)
655
+ throw failure.error;
656
+ }
657
+ class ClientObservation {
658
+ error;
659
+ finished;
660
+ #finish;
661
+ #call;
662
+ #validate;
663
+ constructor(call, validate, options) {
664
+ this.#call = call;
665
+ this.#validate = validate;
666
+ let finish = () => undefined;
667
+ this.finished = new Promise(resolve => {
668
+ finish = resolve;
669
+ });
670
+ this.#finish = finish;
671
+ call.on('metadata', metadata => {
672
+ this.#observe(metadata, options?.onMetadata);
673
+ });
674
+ call.on('status', result => {
675
+ this.#observe(result.metadata, options?.onTrailer);
676
+ this.#finish();
677
+ });
678
+ }
679
+ throwIfInvalid() {
680
+ if (this.error !== undefined)
681
+ throw this.error;
682
+ }
683
+ #observe(metadata, callback) {
684
+ if (this.error !== undefined)
685
+ return;
686
+ try {
687
+ const value = validateMetadata(metadata, this.#validate);
688
+ callback?.(value);
689
+ }
690
+ catch (error) {
691
+ this.error = error;
692
+ this.#call.cancel();
693
+ this.#finish();
694
+ }
695
+ }
696
+ }
697
+ function attachAbort(call, signal) {
698
+ if (signal === undefined)
699
+ return () => undefined;
700
+ const onAbort = () => {
701
+ call.cancel();
702
+ };
703
+ if (signal.aborted)
704
+ onAbort();
705
+ else
706
+ signal.addEventListener('abort', onAbort, { once: true });
707
+ return () => {
708
+ signal.removeEventListener('abort', onAbort);
709
+ };
710
+ }
711
+ //# sourceMappingURL=runtime.js.map