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