@trevrpc/trevrpc-js 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Call, defaultCallOptions, mergeCallOptions } from "./call.js";
2
- import { marshalMessage, unmarshalMessage } from "./framing.js";
2
+ import { FrameType, marshalMessage, unmarshalMessage } from "./framing.js";
3
3
  import { validateMetadata } from "./metadata.js";
4
- import { Code, internal, statusError } from "./status.js";
5
- import { RpcKind, RpcResponse, RpcStreamFrameKind } from "./wire.js";
4
+ import { Code, cancelled, internal, statusError } from "./status.js";
5
+ import { RpcHeaders, RpcKind, RpcStatus } from "./wire.js";
6
6
 
7
7
  /** Creates a service client from a generated service descriptor. */
8
8
  export function createServiceClient(transport, service, root, options = {}) {
@@ -11,18 +11,17 @@ export function createServiceClient(transport, service, root, options = {}) {
11
11
 
12
12
  for (const [jsName, method] of Object.entries(service.methods)) {
13
13
  const rpc = {
14
- service: service.fullName,
15
- method: method.name,
14
+ // The wire method path, computed once per method rather than per call.
15
+ path: `/${service.fullName}/${method.name}`,
16
16
  requestType: root.lookupType(method.inputType),
17
17
  responseType: root.lookupType(method.outputType),
18
+ datagrams: datagramUse(method),
18
19
  };
19
20
  const resolve = (override) => mergeCallOptions(defaults, override);
20
21
 
21
22
  switch (method.kind) {
22
23
  case "unary":
23
24
  client[jsName] = async (request, override) =>
24
- (await unaryCall(transport, rpc, request, resolve(override))).message;
25
- client[`${jsName}WithResponse`] = (request, override) =>
26
25
  unaryCall(transport, rpc, request, resolve(override));
27
26
  break;
28
27
  case "serverStreaming":
@@ -44,29 +43,14 @@ export function createServiceClient(transport, service, root, options = {}) {
44
43
  return client;
45
44
  }
46
45
 
47
- /** Calls a unary RPC and returns the decoded response with its metadata. */
46
+ /** Calls a unary RPC and returns the decoded response. */
48
47
  export async function unaryCall(transport, rpc, request, options) {
49
48
  const body = marshalMessage(rpc.requestType, request);
50
- const call = await Call.open(transport, requestHeader(rpc, RpcKind.Unary, body), options);
51
-
52
- let response;
53
- try {
54
- // The response decides the outcome, so the request side finishes in the background.
55
- call.closeSend().catch(() => {});
56
- response = await call.readFrame(RpcResponse);
57
- validateResponseMetadata(response.metadata);
58
- } catch (error) {
59
- throw call.fail(error);
60
- }
61
- call.finish();
62
-
63
- if (response.status !== Code.Ok) {
64
- throw statusError(response.status, response.message, response.metadata);
65
- }
66
- return {
67
- message: unmarshalMessage(rpc.responseType, response.body),
68
- metadata: response.metadata,
69
- };
49
+ const call = await Call.open(transport, requestFields(rpc, RpcKind.Unary), body, options);
50
+ const stream = new BidirectionalStreamingCall(call, rpc, { options });
51
+ // The response decides the outcome, so the request side finishes in the background.
52
+ stream.closeSend().catch(() => {});
53
+ return await receiveSingleResponse(stream, "unary");
70
54
  }
71
55
 
72
56
  /** Calls a server-streaming RPC and returns its response stream. */
@@ -74,10 +58,12 @@ export async function serverStreamingCall(transport, rpc, request, options) {
74
58
  const body = marshalMessage(rpc.requestType, request);
75
59
  const call = await Call.open(
76
60
  transport,
77
- requestHeader(rpc, RpcKind.ServerStreaming, body),
61
+ requestFields(rpc, RpcKind.ServerStreaming),
62
+ body,
78
63
  options,
64
+ rpc.datagrams,
79
65
  );
80
- const stream = new BidirectionalStreamingCall(call, rpc);
66
+ const stream = new BidirectionalStreamingCall(call, rpc, { options });
81
67
  stream.closeSend().catch(() => {});
82
68
  return new ResponseStream(stream);
83
69
  }
@@ -86,37 +72,142 @@ export async function serverStreamingCall(transport, rpc, request, options) {
86
72
  export async function clientStreamingCall(transport, rpc, options) {
87
73
  const call = await Call.open(
88
74
  transport,
89
- requestHeader(rpc, RpcKind.ClientStreaming, new Uint8Array(0)),
75
+ requestFields(rpc, RpcKind.ClientStreaming),
76
+ undefined,
90
77
  options,
78
+ rpc.datagrams,
79
+ );
80
+ return new ClientStreamingCall(
81
+ new BidirectionalStreamingCall(call, rpc, { streamsRequest: true, options }),
91
82
  );
92
- return new ClientStreamingCall(new BidirectionalStreamingCall(call, rpc));
93
83
  }
94
84
 
95
85
  /** Calls a bidirectional-streaming RPC. */
96
86
  export async function bidirectionalStreamingCall(transport, rpc, options) {
97
87
  const call = await Call.open(
98
88
  transport,
99
- requestHeader(rpc, RpcKind.BidirectionalStreaming, new Uint8Array(0)),
89
+ requestFields(rpc, RpcKind.BidirectionalStreaming),
90
+ undefined,
100
91
  options,
92
+ rpc.datagrams,
101
93
  );
102
- return new BidirectionalStreamingCall(call, rpc);
94
+ return new BidirectionalStreamingCall(call, rpc, { streamsRequest: true, options });
103
95
  }
104
96
 
97
+ /**
98
+ * Receives the single response message of a unary or client-streaming call, then its final
99
+ * status. A second message fails the call immediately; none at all is an internal error.
100
+ */
101
+ async function receiveSingleResponse(stream, kind) {
102
+ let message;
103
+ let count = 0;
104
+ for (;;) {
105
+ const next = await stream.recv();
106
+ if (next === undefined) {
107
+ break;
108
+ }
109
+ count += 1;
110
+ if (count > 1) {
111
+ const error = internal(`${kind} RPC returned more than one response message`);
112
+ await stream.close(error);
113
+ throw error;
114
+ }
115
+ message = next;
116
+ }
117
+ if (count !== 1) {
118
+ throw internal(`${kind} RPC returned no response message`);
119
+ }
120
+ await stream.status;
121
+ return message;
122
+ }
123
+
124
+ /**
125
+ * How much of a response the call reads ahead of recv() while it is still uploading: at most this
126
+ * many frames or bytes are queued before reading pauses and the transport's flow control applies
127
+ * to a caller that does not receive. The byte bound is checked before each read, so the queue can
128
+ * exceed it by at most one frame, which is at most `maxFrameSize`. Without a bound a caller that
129
+ * only sends could be made to buffer a whole response.
130
+ */
131
+ const ReadAheadFrames = 32;
132
+ const ReadAheadBytes = 1 << 20;
133
+
134
+ /**
135
+ * How a method uses datagrams, or undefined if it does not: `send` for request messages, which
136
+ * needs a streaming request and `unreliableRequests`, and `receive` for response messages, which
137
+ * every call with a streaming response accepts. A method needs a flow if it marks either
138
+ * direction unreliable; the generator rejects marks on a direction that does not stream.
139
+ */
140
+ function datagramUse({ kind, unreliableRequests, unreliableResponses }) {
141
+ const streamsRequest = kind === "clientStreaming" || kind === "bidirectionalStreaming";
142
+ const streamsResponse = kind === "serverStreaming" || kind === "bidirectionalStreaming";
143
+ if (!(unreliableRequests || unreliableResponses) || !(streamsRequest || streamsResponse)) {
144
+ return undefined;
145
+ }
146
+ return { send: Boolean(unreliableRequests) && streamsRequest, receive: streamsResponse };
147
+ }
148
+
149
+ const EarlyOk = "the server completed the call before the request stream ended";
150
+
105
151
  /**
106
152
  * A bidirectional-streaming RPC. Messages are received with recv() or async iteration. The call
107
153
  * ends with the server's final status: a non-OK status makes recv() throw it, and `status`
108
- * settles with the final status or the error that ended the call.
154
+ * settles with the final status or the error that ended the call. `headers` resolves once the
155
+ * first response frame arrived; reading it starts that read, so headers are available before the
156
+ * first message.
157
+ *
158
+ * While the request side is open, the response is read ahead of recv() (see ReadAheadFrames), so
159
+ * a final status reaches a caller that is only sending. It stops the upload at once: pending and
160
+ * later sends fail with the non-OK status, or with Cancelled if the status is OK. A call that
161
+ * does not stream its request reads only what recv() or `headers` ask for.
162
+ *
163
+ * The final status settles the outcome as soon as it is read. Responses read before it are still
164
+ * delivered in order, and the call's deadline and abort signal no longer apply to it.
165
+ *
166
+ * The call options' `onResponseHeaders` is called with the headers when the first response frame
167
+ * is read, and `onResponseMetadata` with the trailing metadata when the final status is OK. A
168
+ * callback that throws fails the call.
109
169
  */
110
170
  export class BidirectionalStreamingCall {
111
171
  #call;
112
172
  #rpc;
173
+ /** Whether the request side streams, so that responses are read ahead of recv(). */
174
+ #streamsRequest;
113
175
  /** Serializes recv() so concurrent callers never interleave frame reads. */
114
176
  #reads = Promise.resolve();
115
- #settled = false;
116
- #error = null;
117
- #uploadError = null;
177
+ /** Response messages read ahead of recv(), in order. */
178
+ #queue = [];
179
+ #queuedBytes = 0;
180
+ /**
181
+ * What ends the response, delivered by recv() after the queued messages: `{ status, error }`
182
+ * for the final status (error unless OK) or `{ error }` for a failed stream.
183
+ */
184
+ #terminal;
185
+ /** An error that ended the call out of order, such as an abort; queued messages are dropped. */
186
+ #failure;
187
+ #failingInOrder = false;
188
+ #outcomeSettled = false;
189
+ #pumping = false;
190
+ /**
191
+ * The call's datagram flow when its responses may arrive as datagrams, else undefined. The flow
192
+ * connects once HEADERS (or the first frame that implies them) was read; until then it buffers.
193
+ */
194
+ #flow;
195
+ /** Receivers waiting for a frame. */
196
+ #demand = 0;
197
+ #headersWanted = false;
198
+ #sawHeaders = false;
199
+ #sawMessage = false;
200
+ /** Settles on every change a waiter may care about. */
201
+ #changed = deferred();
118
202
  #resolveStatus;
119
203
  #rejectStatus;
204
+ #resolveHeaders;
205
+ #rejectHeaders;
206
+ #headers;
207
+ #onResponseHeaders;
208
+ #onResponseMetadata;
209
+ /** Whether the headers were handed to onResponseHeaders. */
210
+ #headersReported = false;
120
211
 
121
212
  /** The final status, or the error that ended the call. */
122
213
  status = new Promise((resolve, reject) => {
@@ -124,27 +215,101 @@ export class BidirectionalStreamingCall {
124
215
  this.#rejectStatus = reject;
125
216
  });
126
217
 
127
- constructor(call, rpc) {
218
+ constructor(call, rpc, { streamsRequest = false, options = {} } = {}) {
128
219
  this.#call = call;
129
220
  this.#rpc = rpc;
221
+ this.#streamsRequest = streamsRequest;
222
+ this.#onResponseHeaders = options.onResponseHeaders;
223
+ this.#onResponseMetadata = options.onResponseMetadata;
224
+ this.#flow = rpc.datagrams?.receive ? call.flow : undefined;
130
225
  this.status.catch(() => {});
131
- call.signal.addEventListener("abort", () => this.#settleFailure(call.signal.reason), {
132
- once: true,
226
+ this.#headers = new Promise((resolve, reject) => {
227
+ this.#resolveHeaders = resolve;
228
+ this.#rejectHeaders = reject;
133
229
  });
230
+ this.#headers.catch(() => {});
231
+ call.signal.addEventListener(
232
+ "abort",
233
+ () => {
234
+ if (!this.#failingInOrder) {
235
+ this.#settleFailure(call.signal.reason);
236
+ }
237
+ },
238
+ { once: true },
239
+ );
134
240
  if (call.signal.aborted) {
135
241
  this.#settleFailure(call.signal.reason);
242
+ } else {
243
+ this.#kick();
136
244
  }
137
245
  }
138
246
 
139
- /** Sends one request message. */
247
+ /**
248
+ * The response headers, empty when the server sent none. Settles when the first response frame
249
+ * arrives, or rejects with the error that ended the call first.
250
+ */
251
+ get headers() {
252
+ this.#headersWanted = true;
253
+ this.#kick();
254
+ return this.#headers;
255
+ }
256
+
257
+ /**
258
+ * Sends one request message. Fails with the server's final status if the server ended the
259
+ * call, as soon as it is known; with Cancelled if that status is OK; and with the error that
260
+ * ended the call otherwise.
261
+ */
140
262
  async send(message) {
141
263
  const body = marshalMessage(this.#rpc.requestType, message);
142
- await this.#recordUploadError(this.#call.sendMessage(body));
264
+ try {
265
+ await this.#call.sendMessage(body);
266
+ } catch (error) {
267
+ throw await this.#explainUploadFailure(error);
268
+ }
143
269
  }
144
270
 
145
- /** Closes the request stream while keeping the response stream readable. */
271
+ /**
272
+ * Closes the request stream while keeping the response stream readable. Resolves if the server
273
+ * already ended the call: its final status is reported by recv().
274
+ */
146
275
  async closeSend() {
147
- await this.#recordUploadError(this.#call.closeSend());
276
+ try {
277
+ await this.#call.closeSend();
278
+ } catch (error) {
279
+ const reason = await this.#explainUploadFailure(error);
280
+ if (this.#terminal?.status === undefined) {
281
+ throw reason;
282
+ }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Sends every message of requests, an iterable or async iterable, then closes the request
288
+ * stream. If the server ends the call first, the rest is not sent and nothing is thrown: the
289
+ * response side reports the outcome. A send that fails otherwise rejects as send() does, and
290
+ * requests that throw cancel the call with that error and reject with it.
291
+ */
292
+ async sendAll(requests) {
293
+ let sendFailure;
294
+ try {
295
+ for await (const request of requests) {
296
+ try {
297
+ await this.send(request);
298
+ } catch (error) {
299
+ if (this.#terminal?.status !== undefined) {
300
+ return; // the server ended the call; the response side reports how
301
+ }
302
+ sendFailure = error;
303
+ throw error;
304
+ }
305
+ }
306
+ } catch (error) {
307
+ if (error !== sendFailure) {
308
+ await this.close(error);
309
+ }
310
+ throw error;
311
+ }
312
+ await this.closeSend();
148
313
  }
149
314
 
150
315
  /** Receives the next response message, or undefined after the final OK status. */
@@ -156,8 +321,9 @@ export class BidirectionalStreamingCall {
156
321
 
157
322
  /** Cancels the call unless it already finished. */
158
323
  async close(reason) {
159
- if (!this.#settled) {
324
+ if (this.#terminal === undefined || this.#queue.length > 0) {
160
325
  this.#call.cancel(reason);
326
+ this.#settleFailure(this.#call.signal.reason);
161
327
  }
162
328
  }
163
329
 
@@ -175,56 +341,267 @@ export class BidirectionalStreamingCall {
175
341
  }
176
342
 
177
343
  async #recv() {
178
- if (this.#error != null) {
179
- throw this.#error;
344
+ const item = await this.#take();
345
+ if (item.payload !== undefined) {
346
+ try {
347
+ return unmarshalMessage(this.#rpc.responseType, item.payload);
348
+ } catch (error) {
349
+ throw this.#settleFailure(this.#call.fail(error));
350
+ }
180
351
  }
181
- if (this.#settled) {
182
- return undefined;
352
+ if (item.error !== undefined) {
353
+ throw item.error;
354
+ }
355
+ return undefined;
356
+ }
357
+
358
+ /** Takes the next queued message, or the end of the response once the queue is empty. */
359
+ async #take() {
360
+ for (;;) {
361
+ if (this.#failure !== undefined) {
362
+ throw this.#failure;
363
+ }
364
+ const payload = this.#queue.shift();
365
+ if (payload !== undefined) {
366
+ this.#queuedBytes -= payload.byteLength;
367
+ // Room for more read-ahead.
368
+ this.#kick();
369
+ return { payload };
370
+ }
371
+ if (this.#terminal !== undefined) {
372
+ // A failure of the stream itself surfaces here, in order, after the responses before it.
373
+ if (this.#terminal.status === undefined) {
374
+ throw this.#settleFailure(this.#terminal.error);
375
+ }
376
+ return this.#terminal;
377
+ }
378
+ const changed = this.#changed.promise;
379
+ this.#demand += 1;
380
+ this.#kick();
381
+ try {
382
+ await changed;
383
+ } finally {
384
+ this.#demand -= 1;
385
+ }
183
386
  }
387
+ }
184
388
 
185
- let frame;
389
+ /** Starts reading the response unless it is running or has nothing to read for. */
390
+ #kick() {
391
+ if (!this.#pumping && this.#wantsRead()) {
392
+ this.#pumping = true;
393
+ // The pump handles the errors of the response itself; anything else is a bug that ends the call.
394
+ this.#pump().catch((error) => this.#settleFailure(this.#call.fail(error)));
395
+ }
396
+ }
397
+
398
+ #wantsRead() {
399
+ if (this.#terminal !== undefined || this.#failure !== undefined) {
400
+ return false;
401
+ }
402
+ if (this.#demand > 0 && this.#queue.length === 0) {
403
+ return true;
404
+ }
405
+ if (this.#headersWanted && !this.#sawHeaders && !this.#sawMessage) {
406
+ return true;
407
+ }
408
+ // A call with a datagram flow always reads ahead, so datagrams are released after HEADERS
409
+ // even if nobody receives.
410
+ return (
411
+ ((this.#streamsRequest && this.#call.uploadOpen) || this.#flow !== undefined) &&
412
+ this.#queue.length < ReadAheadFrames &&
413
+ this.#queuedBytes < ReadAheadBytes
414
+ );
415
+ }
416
+
417
+ /** Reads response events into the queue for as long as someone wants them. */
418
+ async #pump() {
186
419
  try {
187
- frame = await this.#call.readStreamFrame();
188
- if (frame === null) {
189
- throw this.#uploadError ?? internal("response stream ended before final status");
420
+ while (this.#wantsRead()) {
421
+ let event;
422
+ try {
423
+ event = await this.#readEvent();
424
+ } catch (error) {
425
+ this.#endWithError(error);
426
+ return;
427
+ }
428
+ if (this.#failure !== undefined) {
429
+ return;
430
+ }
431
+ if (event.type === FrameType.Message) {
432
+ this.#queue.push(event.payload);
433
+ this.#queuedBytes += event.payload.byteLength;
434
+ } else {
435
+ this.#endWithStatus(event.status);
436
+ }
437
+ this.#notify();
190
438
  }
191
- if (frame.kind === RpcStreamFrameKind.Message) {
192
- return unmarshalMessage(this.#rpc.responseType, frame.body);
439
+ } finally {
440
+ this.#pumping = false;
441
+ this.#notify();
442
+ }
443
+ }
444
+
445
+ /** The final status arrived: it is the outcome, and it ends the upload. */
446
+ #endWithStatus({ code, message, metadata }) {
447
+ if (code === Code.Ok && this.#onResponseMetadata !== undefined) {
448
+ try {
449
+ this.#onResponseMetadata(metadata);
450
+ } catch (error) {
451
+ this.#endWithError(error);
452
+ return;
193
453
  }
194
- validateResponseMetadata(frame.metadata);
195
- } catch (error) {
196
- throw this.#settleFailure(this.#call.fail(error));
197
454
  }
455
+ const error = code === Code.Ok ? undefined : statusError(code, message, metadata);
456
+ this.#terminal = { status: { code, message, metadata }, error };
457
+ this.#call.finish(error ?? cancelled(EarlyOk));
458
+ if (error === undefined) {
459
+ this.#outcomeSettled = true;
460
+ this.#resolveStatus({ code, message, metadata });
461
+ } else {
462
+ this.#rejectOutcome(error);
463
+ }
464
+ }
198
465
 
199
- // A status frame is final: the request side stops and the stream drains in the background.
200
- this.#call.finish();
201
- this.#settled = true;
202
- if (frame.status !== Code.Ok) {
203
- this.#error = statusError(frame.status, frame.message, frame.metadata);
204
- this.#rejectStatus(this.#error);
205
- throw this.#error;
466
+ /**
467
+ * The response stream failed or ended without a final status. Responses read before are still
468
+ * delivered; the error surfaces when recv() reaches it. The call is torn down now, so sends
469
+ * fail with this error.
470
+ */
471
+ #endWithError(error) {
472
+ if (this.#call.signal.aborted) {
473
+ return; // an abort already decided the outcome
474
+ }
475
+ this.#failingInOrder = true;
476
+ const reason = this.#call.fail(error);
477
+ this.#failingInOrder = false;
478
+ this.#terminal = { error: reason };
479
+ this.#rejectOutcome(reason);
480
+ this.#notify();
481
+ }
482
+
483
+ /** Settles the headers on their first frame, and hands them to onResponseHeaders once. */
484
+ #settleHeaders(metadata) {
485
+ this.#resolveHeaders(metadata);
486
+ if (!this.#headersReported) {
487
+ this.#headersReported = true;
488
+ this.#onResponseHeaders?.(metadata);
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Reads the next MESSAGE or STATUS event, consuming HEADERS on the way. Enforces the response
494
+ * grammar `[HEADERS] MESSAGE* STATUS`.
495
+ */
496
+ async #readEvent() {
497
+ for (;;) {
498
+ const frame = await this.#call.readFrame();
499
+ if (frame === null) {
500
+ throw this.#call.uploadFailure ?? internal("response stream ended before final status");
501
+ }
502
+
503
+ switch (frame.type) {
504
+ case FrameType.Headers: {
505
+ if (this.#sawHeaders || this.#sawMessage) {
506
+ throw internal(
507
+ this.#sawHeaders ? "duplicate HEADERS frame" : "HEADERS frame after MESSAGE",
508
+ );
509
+ }
510
+ const headers = unmarshalMessage(RpcHeaders, frame.payload);
511
+ validateResponseMetadata(headers.metadata, "headers");
512
+ this.#sawHeaders = true;
513
+ this.#settleHeaders(headers.metadata);
514
+ this.#flow?.connect(this.#onDatagram);
515
+ continue;
516
+ }
517
+ case FrameType.Message:
518
+ this.#sawMessage = true;
519
+ this.#settleHeaders({});
520
+ this.#flow?.connect(this.#onDatagram);
521
+ return frame;
522
+ case FrameType.Status: {
523
+ const status = unmarshalMessage(RpcStatus, frame.payload);
524
+ validateResponseMetadata(status.metadata, "trailing metadata");
525
+ this.#settleHeaders({});
526
+ this.#flow?.connect(this.#onDatagram);
527
+ return { type: FrameType.Status, status };
528
+ }
529
+ default:
530
+ throw internal(`unexpected frame type ${frame.type} on a response stream`);
531
+ }
206
532
  }
207
- this.#resolveStatus({ code: Code.Ok, message: frame.message, metadata: frame.metadata });
208
- return undefined;
209
533
  }
210
534
 
535
+ /**
536
+ * Queues a response message that arrived as a datagram, in arrival order with the stream's
537
+ * messages. It never blocks: a message is dropped when the call ended or its queue is full.
538
+ */
539
+ #onDatagram = (payload) => {
540
+ if (
541
+ this.#terminal !== undefined ||
542
+ this.#failure !== undefined ||
543
+ payload.byteLength > this.#call.maxFrameSize ||
544
+ this.#queue.length >= ReadAheadFrames ||
545
+ this.#queuedBytes + payload.byteLength > ReadAheadBytes
546
+ ) {
547
+ return;
548
+ }
549
+ this.#queue.push(payload);
550
+ this.#queuedBytes += payload.byteLength;
551
+ this.#notify();
552
+ };
553
+
554
+ /** Ends the call out of order: receivers fail with `error` without draining the queue. */
211
555
  #settleFailure(error) {
212
- if (!this.#settled) {
213
- this.#settled = true;
214
- this.#error = error;
556
+ this.#failure ??= error;
557
+ this.#rejectOutcome(error);
558
+ this.#notify();
559
+ return this.#failure;
560
+ }
561
+
562
+ #rejectOutcome(error) {
563
+ if (!this.#outcomeSettled) {
564
+ this.#outcomeSettled = true;
215
565
  this.#rejectStatus(error);
566
+ this.#rejectHeaders(error);
216
567
  }
217
- return this.#error;
218
568
  }
219
569
 
220
- async #recordUploadError(upload) {
221
- try {
222
- await upload;
223
- } catch (error) {
224
- this.#uploadError ??= error;
225
- throw error;
570
+ /** Wakes everything waiting for a change of the response. */
571
+ #notify() {
572
+ const { resolve } = this.#changed;
573
+ this.#changed = deferred();
574
+ resolve();
575
+ }
576
+
577
+ /**
578
+ * Resolves when the response outcome is known, the call failed, or no read is in progress, which
579
+ * is when nothing more will be learned without the caller receiving.
580
+ */
581
+ async #outcomeKnown() {
582
+ while (this.#terminal === undefined && this.#failure === undefined && this.#pumping) {
583
+ await this.#changed.promise;
226
584
  }
227
585
  }
586
+
587
+ /**
588
+ * The error a failed request-side operation reports. A request side that the transport failed
589
+ * on its own, typically because the server stopped reading it, says little: the server's
590
+ * final status, or the failure of the response stream, explains it, so those are awaited.
591
+ */
592
+ async #explainUploadFailure(error) {
593
+ if (error !== this.#call.uploadFailure) {
594
+ return error;
595
+ }
596
+ await this.#outcomeKnown();
597
+ if (this.#terminal?.status !== undefined) {
598
+ return this.#terminal.error ?? cancelled(EarlyOk);
599
+ }
600
+ if (this.#call.signal.aborted) {
601
+ return this.#call.signal.reason;
602
+ }
603
+ return this.#terminal?.error ?? error;
604
+ }
228
605
  }
229
606
 
230
607
  /** A client-streaming RPC: send requests, then close and receive the single response. */
@@ -240,30 +617,15 @@ export class ClientStreamingCall {
240
617
  return this.#call.send(message);
241
618
  }
242
619
 
243
- /** Closes the request stream and returns the final response. */
244
- async closeAndRecv() {
245
- return (await this.closeAndRecvWithResponse()).message;
620
+ /** Sends every message of requests, then closes the request stream; see the bidirectional call. */
621
+ sendAll(requests) {
622
+ return this.#call.sendAll(requests);
246
623
  }
247
624
 
248
- /** Closes the request stream and returns the final response with its metadata. */
249
- async closeAndRecvWithResponse() {
625
+ /** Closes the request stream and returns the final response. */
626
+ async closeAndRecv() {
250
627
  this.#call.closeSend().catch(() => {});
251
-
252
- let message;
253
- let count = 0;
254
- for (;;) {
255
- const next = await this.#call.recv();
256
- if (next === undefined) {
257
- break;
258
- }
259
- message ??= next;
260
- count += 1;
261
- }
262
- if (count !== 1) {
263
- throw internal(`client-streaming RPC returned ${count} response messages instead of one`);
264
- }
265
- const status = await this.#call.status;
266
- return { message, metadata: status.metadata };
628
+ return await receiveSingleResponse(this.#call, "client-streaming");
267
629
  }
268
630
 
269
631
  /** Cancels the call unless it already finished. */
@@ -285,6 +647,14 @@ export class ResponseStream {
285
647
  return this.#call.status;
286
648
  }
287
649
 
650
+ /**
651
+ * The response headers, empty when the server sent none. Settles when the first response frame
652
+ * arrives, even before the first message is received, or rejects with the call error.
653
+ */
654
+ get headers() {
655
+ return this.#call.headers;
656
+ }
657
+
288
658
  /** Cancels the call unless it already finished. */
289
659
  close(reason) {
290
660
  return this.#call.close(reason);
@@ -295,14 +665,22 @@ export class ResponseStream {
295
665
  }
296
666
  }
297
667
 
298
- function requestHeader(rpc, kind, body) {
299
- return { service: rpc.service, method: rpc.method, kind, body };
668
+ function deferred() {
669
+ let resolve;
670
+ const promise = new Promise((resolvePromise) => {
671
+ resolve = resolvePromise;
672
+ });
673
+ return { promise, resolve };
674
+ }
675
+
676
+ function requestFields(rpc, kind) {
677
+ return { method: rpc.path, kind };
300
678
  }
301
679
 
302
- function validateResponseMetadata(metadata) {
680
+ function validateResponseMetadata(metadata, what) {
303
681
  try {
304
682
  validateMetadata(metadata);
305
683
  } catch (error) {
306
- throw internal(`invalid response metadata: ${error.statusMessage ?? error.message}`);
684
+ throw internal(`invalid response ${what}: ${error.statusMessage ?? error.message}`);
307
685
  }
308
686
  }