@trevrpc/trevrpc-js 0.2.3 → 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/README.md +127 -46
- package/package.json +12 -45
- package/src/bytes.js +10 -0
- package/src/call.js +520 -0
- package/src/channel.js +258 -288
- package/src/client.js +543 -1254
- package/src/datagram.js +336 -0
- package/src/framing.js +151 -548
- package/src/generator.js +66 -109
- package/src/index.d.ts +235 -463
- package/src/index.js +4 -3
- package/src/qmux/frames.js +262 -0
- package/src/qmux/params.js +104 -0
- package/src/qmux/session.js +739 -0
- package/src/race.js +90 -0
- package/src/status.js +24 -70
- package/src/varint.js +73 -0
- package/src/wire.js +21 -89
- package/examples/greeter/README.md +0 -35
- package/examples/greeter/client.js +0 -172
- package/examples/greeter/greeter.node.trevrpc.d.ts +0 -53
- package/examples/greeter/greeter.node.trevrpc.js +0 -9
- package/examples/greeter/greeter.proto +0 -18
- package/examples/greeter/greeter.trevrpc.d.ts +0 -55
- package/examples/greeter/greeter.trevrpc.js +0 -103
- package/examples/greeter/index.html +0 -140
- package/examples/greeter/static-server.js +0 -168
- package/src/advanced.d.ts +0 -29
- package/src/advanced.js +0 -1
- package/src/browser-channel.js +0 -49
- package/src/connect.js +0 -6
- package/src/connect.node.js +0 -6
- package/src/index.node.d.ts +0 -10
- package/src/index.node.js +0 -3
- package/src/native-loader.js +0 -118
- package/src/node-advanced.d.ts +0 -27
- package/src/node-advanced.js +0 -1
- package/src/node-generated.d.ts +0 -24
- package/src/node-generated.js +0 -126
- package/src/node-index.d.ts +0 -1
- package/src/node-index.js +0 -7
- package/src/node.d.ts +0 -298
- package/src/node.js +0 -1456
- package/src/runtime.js +0 -66
- package/src/server-response.js +0 -49
- package/src/timer.js +0 -29
- package/src/webtransport.js +0 -639
package/src/call.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { datagramEndpoint, messageFrameSize } from "./datagram.js";
|
|
2
|
+
import {
|
|
3
|
+
DefaultMaxFrameSize,
|
|
4
|
+
FrameReader,
|
|
5
|
+
encodeMessageFrame,
|
|
6
|
+
encodeRequestFrame,
|
|
7
|
+
} from "./framing.js";
|
|
8
|
+
import { normalizeMetadata, validateMetadata } from "./metadata.js";
|
|
9
|
+
import {
|
|
10
|
+
Code,
|
|
11
|
+
TrevRpcError,
|
|
12
|
+
cancelled,
|
|
13
|
+
deadlineExceeded,
|
|
14
|
+
failedPrecondition,
|
|
15
|
+
invalidArgument,
|
|
16
|
+
statusError,
|
|
17
|
+
statusFromTransportError,
|
|
18
|
+
} from "./status.js";
|
|
19
|
+
|
|
20
|
+
/** Longest delay setTimeout can represent; it fires immediately beyond this. */
|
|
21
|
+
const MaxTimerDelayMs = 2_147_483_647;
|
|
22
|
+
const MaxUint64 = 0xffffffffffffffffn;
|
|
23
|
+
|
|
24
|
+
/** Returns the default call options. */
|
|
25
|
+
export function defaultCallOptions() {
|
|
26
|
+
return {
|
|
27
|
+
timeoutMs: undefined,
|
|
28
|
+
maxFrameSize: DefaultMaxFrameSize,
|
|
29
|
+
metadata: {},
|
|
30
|
+
signal: undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Merges override options over resolved base options; metadata merges per key. */
|
|
35
|
+
export function mergeCallOptions(base, override = {}) {
|
|
36
|
+
const merged = { ...base };
|
|
37
|
+
for (const [key, value] of Object.entries(override)) {
|
|
38
|
+
if (value !== undefined && key !== "metadata") {
|
|
39
|
+
merged[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (override.metadata != null) {
|
|
43
|
+
merged.metadata = Object.assign(
|
|
44
|
+
Object.create(null),
|
|
45
|
+
base.metadata,
|
|
46
|
+
normalizeMetadata(override.metadata),
|
|
47
|
+
);
|
|
48
|
+
validateMetadata(merged.metadata);
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Cancellation and deadline for one call. The signal aborts with a TrevRpcError reason: the
|
|
55
|
+
* failure that ended the call, the caller's own status error, DeadlineExceeded, or Cancelled
|
|
56
|
+
* wrapping any other abort reason.
|
|
57
|
+
*/
|
|
58
|
+
class CallScope {
|
|
59
|
+
#controller = new AbortController();
|
|
60
|
+
#parent;
|
|
61
|
+
#timer;
|
|
62
|
+
|
|
63
|
+
/** timeoutMs must be validated by the caller. */
|
|
64
|
+
constructor({ signal, timeoutMs }) {
|
|
65
|
+
if (signal?.aborted) {
|
|
66
|
+
this.abort(signal.reason);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (signal != null) {
|
|
70
|
+
this.#parent = { signal, listener: () => this.abort(signal.reason) };
|
|
71
|
+
signal.addEventListener("abort", this.#parent.listener, { once: true });
|
|
72
|
+
}
|
|
73
|
+
if (timeoutMs != null && timeoutMs <= MaxTimerDelayMs) {
|
|
74
|
+
this.#timer = setTimeout(
|
|
75
|
+
() => this.abort(deadlineExceeded("RPC deadline exceeded")),
|
|
76
|
+
timeoutMs,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get signal() {
|
|
82
|
+
return this.#controller.signal;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Aborts the call; only the first reason counts. Returns the effective reason. */
|
|
86
|
+
abort(reason) {
|
|
87
|
+
if (!this.signal.aborted) {
|
|
88
|
+
this.settle();
|
|
89
|
+
this.#controller.abort(
|
|
90
|
+
reason instanceof TrevRpcError ? reason : cancelled("RPC cancelled", reason),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return this.signal.reason;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Stops the deadline and detaches from the caller's signal once the outcome is known. */
|
|
97
|
+
settle() {
|
|
98
|
+
clearTimeout(this.#timer);
|
|
99
|
+
this.#parent?.signal.removeEventListener("abort", this.#parent.listener);
|
|
100
|
+
this.#parent = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One RPC over one bidirectional stream: the request writer, the response frame reader, and the
|
|
106
|
+
* call's cancellation scope. Transport failures surface as TrevRpcError, and once the call is
|
|
107
|
+
* aborted every operation fails with the abort reason.
|
|
108
|
+
*/
|
|
109
|
+
export class Call {
|
|
110
|
+
#scope;
|
|
111
|
+
#writer;
|
|
112
|
+
#cancelReader;
|
|
113
|
+
#frames;
|
|
114
|
+
#maxFrameSize;
|
|
115
|
+
#sendClosed = false;
|
|
116
|
+
/** Set once the close of the request side completed. */
|
|
117
|
+
#closeCompleted = false;
|
|
118
|
+
/**
|
|
119
|
+
* The error every request-side operation fails with once the upload was ended, or undefined
|
|
120
|
+
* while it is open.
|
|
121
|
+
*/
|
|
122
|
+
#uploadEnded;
|
|
123
|
+
/** Rejectors of the request-side operations waiting for the transport. */
|
|
124
|
+
#pendingUploads = new Set();
|
|
125
|
+
/** The transport error that made the request side fail on its own, or undefined. */
|
|
126
|
+
#uploadFailure;
|
|
127
|
+
/** The call's datagram flow, or undefined if its messages all travel on the stream. */
|
|
128
|
+
#flow;
|
|
129
|
+
/** Whether request messages are sent as datagrams when they fit. */
|
|
130
|
+
#sendDatagrams = false;
|
|
131
|
+
/** Request messages of a call that uses datagrams, queued for the flush loop, in order. */
|
|
132
|
+
#unsent = [];
|
|
133
|
+
/** Settles when the flush loop has sent everything queued so far. */
|
|
134
|
+
#sendTail = Promise.resolve();
|
|
135
|
+
#flushing = false;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Opens a stream on the transport and sends the REQUEST frame. The request carries the
|
|
139
|
+
* method path and kind; metadata and the wire timeout come from the options. The request
|
|
140
|
+
* message of a unary or server-streaming call goes out in the same write.
|
|
141
|
+
*
|
|
142
|
+
* `datagrams` is `{ send, receive }` for a method that marks a direction unreliable: the call
|
|
143
|
+
* gets a datagram flow if the session that carries it has datagrams, which is only known once
|
|
144
|
+
* its stream is open, so the REQUEST is encoded after that. `send` makes it send request
|
|
145
|
+
* messages as datagrams, `receive` makes it accept response datagrams.
|
|
146
|
+
*/
|
|
147
|
+
static async open(transport, request, message, options, datagrams) {
|
|
148
|
+
const fields = { ...request, metadata: options.metadata, ...timeoutField(options) };
|
|
149
|
+
const header =
|
|
150
|
+
datagrams === undefined ? encodeRequestFrame(fields, message, options.maxFrameSize) : null;
|
|
151
|
+
const scope = new CallScope(options);
|
|
152
|
+
let call;
|
|
153
|
+
try {
|
|
154
|
+
const session = datagrams === undefined ? undefined : {};
|
|
155
|
+
const stream = await openStream(transport, scope.signal, session);
|
|
156
|
+
call = new Call(stream, scope, options.maxFrameSize);
|
|
157
|
+
await call.#write(header ?? call.#requestWithFlow(fields, message, session, datagrams));
|
|
158
|
+
return call;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (call !== undefined) {
|
|
161
|
+
call.#releaseFlow();
|
|
162
|
+
}
|
|
163
|
+
throw scope.abort(failureStatus(error));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Opens the call's flow on its session, if it has datagrams, and encodes the REQUEST. */
|
|
168
|
+
#requestWithFlow(fields, message, session, { send, receive }) {
|
|
169
|
+
this.#flow = datagramEndpoint(session.datagrams)?.openFlow({ receive });
|
|
170
|
+
this.#sendDatagrams = this.#flow !== undefined && send;
|
|
171
|
+
return encodeRequestFrame(
|
|
172
|
+
this.#flow === undefined ? fields : { ...fields, datagramFlow: this.#flow.id },
|
|
173
|
+
message,
|
|
174
|
+
this.#maxFrameSize,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
constructor(stream, scope, maxFrameSize) {
|
|
179
|
+
const reader = stream.readable.getReader();
|
|
180
|
+
this.#scope = scope;
|
|
181
|
+
this.#writer = stream.writable.getWriter();
|
|
182
|
+
this.#cancelReader = (reason) => reader.cancel(reason);
|
|
183
|
+
this.#frames = new FrameReader(reader);
|
|
184
|
+
this.#maxFrameSize = maxFrameSize;
|
|
185
|
+
// The transport errors the request side as soon as the server stops reading it.
|
|
186
|
+
this.#writer.closed.catch((error) => {
|
|
187
|
+
if (this.#uploadEnded === undefined) {
|
|
188
|
+
this.#uploadFailure ??= statusFromTransportError(error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
scope.signal.addEventListener("abort", () => this.#teardown(), { once: true });
|
|
192
|
+
if (scope.signal.aborted) {
|
|
193
|
+
this.#teardown();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The call's abort signal. */
|
|
198
|
+
get signal() {
|
|
199
|
+
return this.#scope.signal;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The transport error that made the request side fail on its own, for instance because the
|
|
204
|
+
* server stopped reading it; undefined while the request side works or was ended by the call.
|
|
205
|
+
*/
|
|
206
|
+
get uploadFailure() {
|
|
207
|
+
return this.#uploadFailure;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Whether the request side can still be written: not ended, not closed. */
|
|
211
|
+
get uploadOpen() {
|
|
212
|
+
return this.#uploadEnded === undefined && !this.#closeCompleted;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The largest frame the call accepts and sends, in bytes. */
|
|
216
|
+
get maxFrameSize() {
|
|
217
|
+
return this.#maxFrameSize;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The call's datagram flow, or undefined if its messages all travel on the stream. */
|
|
221
|
+
get flow() {
|
|
222
|
+
return this.#flow;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Sends one streaming request message; writes are ordered and apply backpressure. Fails with
|
|
227
|
+
* the error that ended the upload, if any, even while the transport itself is still blocked.
|
|
228
|
+
* On a call that sends datagrams the message goes out as one if it fits, and on the stream
|
|
229
|
+
* otherwise.
|
|
230
|
+
*/
|
|
231
|
+
sendMessage(body) {
|
|
232
|
+
if (this.#uploadEnded !== undefined) {
|
|
233
|
+
return Promise.reject(this.#uploadEnded);
|
|
234
|
+
}
|
|
235
|
+
if (this.#sendClosed) {
|
|
236
|
+
return Promise.reject(failedPrecondition("request stream is closed"));
|
|
237
|
+
}
|
|
238
|
+
if (this.#sendDatagrams) {
|
|
239
|
+
return this.#upload(
|
|
240
|
+
new Promise((resolve, reject) => {
|
|
241
|
+
this.#unsent.push({ body, resolve, reject });
|
|
242
|
+
if (!this.#flushing) {
|
|
243
|
+
this.#flushing = true;
|
|
244
|
+
// Messages sent in the same turn are packed together; nothing waits for a timer.
|
|
245
|
+
this.#sendTail = Promise.resolve().then(() => this.#flush());
|
|
246
|
+
}
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return this.#write(encodeMessageFrame(body, this.#maxFrameSize));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Sends the queued request messages in order: as many as fit into one datagram go together, a
|
|
255
|
+
* message that cannot be a datagram goes on the stream, and so does every message of a datagram
|
|
256
|
+
* that failed to send. Messages queued while a datagram waits for send-buffer space join the
|
|
257
|
+
* next one.
|
|
258
|
+
*/
|
|
259
|
+
async #flush() {
|
|
260
|
+
let inFlight = [];
|
|
261
|
+
try {
|
|
262
|
+
while (this.#unsent.length > 0) {
|
|
263
|
+
if (this.#uploadEnded !== undefined) {
|
|
264
|
+
throw this.#uploadEnded;
|
|
265
|
+
}
|
|
266
|
+
inFlight = this.#takeBatch();
|
|
267
|
+
if (inFlight.length === 0 || !(await this.#flow.send(inFlight.map(bodyOf), this.signal))) {
|
|
268
|
+
// Not a datagram: a head message that never fits, or a datagram that failed to send.
|
|
269
|
+
inFlight = inFlight.length > 0 ? inFlight : [this.#unsent.shift()];
|
|
270
|
+
for (const { body } of inFlight) {
|
|
271
|
+
await this.#writer.write(encodeMessageFrame(body, this.#maxFrameSize));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const item of inFlight) {
|
|
275
|
+
item.resolve();
|
|
276
|
+
}
|
|
277
|
+
inFlight = [];
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
for (const item of [...inFlight, ...this.#unsent.splice(0)]) {
|
|
281
|
+
item.reject(error);
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
this.#flushing = false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Removes the longest run of queued messages from the front that fits one datagram, at least
|
|
290
|
+
* one if the head fits alone; empty if the head can only go on the stream.
|
|
291
|
+
*/
|
|
292
|
+
#takeBatch() {
|
|
293
|
+
let room = this.#flow.frameRoom;
|
|
294
|
+
let count = 0;
|
|
295
|
+
while (count < this.#unsent.length) {
|
|
296
|
+
const { body } = this.#unsent[count];
|
|
297
|
+
const size = messageFrameSize(body);
|
|
298
|
+
if (body.byteLength > this.#maxFrameSize || size > room) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
room -= size;
|
|
302
|
+
count += 1;
|
|
303
|
+
}
|
|
304
|
+
return this.#unsent.splice(0, count);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Finishes the request stream gracefully. */
|
|
308
|
+
closeSend() {
|
|
309
|
+
if (this.#sendClosed) {
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
}
|
|
312
|
+
this.#sendClosed = true;
|
|
313
|
+
// Datagram sends in flight go first, so the stream does not end before them.
|
|
314
|
+
const closing = this.#sendDatagrams
|
|
315
|
+
? this.#sendTail.then(() => this.#writer.close())
|
|
316
|
+
: this.#writer.close();
|
|
317
|
+
return this.#upload(closing).then(() => {
|
|
318
|
+
this.#closeCompleted = true;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Reads the next known response frame as `{ type, payload }`, or null if the stream ended
|
|
324
|
+
* cleanly. Unknown frames are skipped.
|
|
325
|
+
*/
|
|
326
|
+
readFrame() {
|
|
327
|
+
return this.#guard(this.#frames.readFrame(this.#maxFrameSize));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Ends the call after its final status was received. The request side stops: pending and later
|
|
332
|
+
* sends fail with `uploadError`, and an unfinished upload is reset. The rest of the response
|
|
333
|
+
* stream is drained in the background: unknown frames are skipped, and a known frame after
|
|
334
|
+
* STATUS cancels the stream without changing the outcome.
|
|
335
|
+
*/
|
|
336
|
+
finish(uploadError) {
|
|
337
|
+
this.#scope.settle();
|
|
338
|
+
this.#releaseFlow();
|
|
339
|
+
this.#endUpload(uploadError, cancelled("RPC completed"));
|
|
340
|
+
this.#frames.expectEnd(this.#maxFrameSize).catch(() => this.#cancelReader().catch(() => {}));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Cancels the call on behalf of the caller; any reason becomes a Cancelled status. */
|
|
344
|
+
cancel(reason) {
|
|
345
|
+
this.#scope.abort(reason);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Fails the call, tearing down both stream directions. Returns the effective error. */
|
|
349
|
+
fail(error) {
|
|
350
|
+
return this.#scope.abort(failureStatus(error));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#write(bytes) {
|
|
354
|
+
return this.#upload(this.#writer.write(bytes));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Waits for a write or close of the request side. Ending the upload settles it at once: a
|
|
359
|
+
* transport blocked on flow control does not act on an abort while the write is in flight.
|
|
360
|
+
*/
|
|
361
|
+
#upload(operation) {
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
this.#pendingUploads.add(reject);
|
|
364
|
+
operation.then(
|
|
365
|
+
(value) => {
|
|
366
|
+
this.#pendingUploads.delete(reject);
|
|
367
|
+
if (this.signal.aborted) {
|
|
368
|
+
reject(this.signal.reason);
|
|
369
|
+
} else {
|
|
370
|
+
resolve(value);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
(error) => {
|
|
374
|
+
this.#pendingUploads.delete(reject);
|
|
375
|
+
reject(this.#uploadError(error));
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Maps a failed request-side operation to the call's error. */
|
|
382
|
+
#uploadError(error) {
|
|
383
|
+
if (this.signal.aborted) {
|
|
384
|
+
return this.signal.reason;
|
|
385
|
+
}
|
|
386
|
+
if (error === this.#uploadEnded) {
|
|
387
|
+
return error;
|
|
388
|
+
}
|
|
389
|
+
this.#uploadFailure ??= statusFromTransportError(error);
|
|
390
|
+
return this.#uploadFailure;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Stops the request side with `error` for everything waiting on it; only the first call counts. */
|
|
394
|
+
#endUpload(error, abortReason = error) {
|
|
395
|
+
if (this.#uploadEnded !== undefined) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.#uploadEnded = error;
|
|
399
|
+
this.#sendClosed = true;
|
|
400
|
+
this.#writer.abort(abortReason).catch(() => {});
|
|
401
|
+
const pending = [...this.#pendingUploads];
|
|
402
|
+
this.#pendingUploads.clear();
|
|
403
|
+
for (const reject of pending) {
|
|
404
|
+
reject(error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async #guard(operation) {
|
|
409
|
+
let result;
|
|
410
|
+
try {
|
|
411
|
+
result = await operation;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
throw this.signal.aborted ? this.signal.reason : statusFromTransportError(error);
|
|
414
|
+
}
|
|
415
|
+
// Cancelling the reader completes pending reads as end-of-stream instead of rejecting them.
|
|
416
|
+
throwIfAborted(this.signal);
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Ends the datagram flow, if any; every way a call ends gets here. */
|
|
421
|
+
#releaseFlow() {
|
|
422
|
+
this.#flow?.release();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#teardown() {
|
|
426
|
+
this.#releaseFlow();
|
|
427
|
+
const reason = this.signal.reason;
|
|
428
|
+
this.#endUpload(reason);
|
|
429
|
+
this.#cancelReader(reason).catch(() => {});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Opens a bidirectional stream. A call that may use datagrams passes `session`, which receives the
|
|
435
|
+
* datagram object of the session that opened the stream, so the flow belongs to the connection
|
|
436
|
+
* that carries the call and not to one a reconnect swapped in later.
|
|
437
|
+
*/
|
|
438
|
+
function openStream(transport, signal, session) {
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
if (signal.aborted) {
|
|
441
|
+
reject(signal.reason);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const onAbort = () => reject(signal.reason);
|
|
445
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
446
|
+
|
|
447
|
+
let opening;
|
|
448
|
+
try {
|
|
449
|
+
opening = Promise.resolve(startStream(transport, session));
|
|
450
|
+
} catch (error) {
|
|
451
|
+
opening = Promise.reject(error);
|
|
452
|
+
}
|
|
453
|
+
opening.then(
|
|
454
|
+
(stream) => {
|
|
455
|
+
signal.removeEventListener("abort", onAbort);
|
|
456
|
+
if (signal.aborted) {
|
|
457
|
+
// The caller gave up while the stream was opening; do not leak it.
|
|
458
|
+
stream.writable.abort(signal.reason).catch(() => {});
|
|
459
|
+
stream.readable.cancel(signal.reason).catch(() => {});
|
|
460
|
+
} else {
|
|
461
|
+
resolve(stream);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
(error) => {
|
|
465
|
+
signal.removeEventListener("abort", onAbort);
|
|
466
|
+
reject(statusFromTransportError(error));
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function bodyOf(item) {
|
|
473
|
+
return item.body;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function startStream(transport, session) {
|
|
477
|
+
if (session === undefined) {
|
|
478
|
+
return transport.createBidirectionalStream();
|
|
479
|
+
}
|
|
480
|
+
if (typeof transport.createBidirectionalStreamWithDatagrams === "function") {
|
|
481
|
+
return transport.createBidirectionalStreamWithDatagrams().then(({ stream, datagrams }) => {
|
|
482
|
+
session.datagrams = datagrams;
|
|
483
|
+
return stream;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
// A WebTransport session used directly: its datagrams belong to the stream opened right now.
|
|
487
|
+
session.datagrams = transport.datagrams;
|
|
488
|
+
return transport.createBidirectionalStream();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function throwIfAborted(signal) {
|
|
492
|
+
if (signal.aborted) {
|
|
493
|
+
throw signal.reason;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Status errors describe themselves; anything else reaching the call is an internal bug. */
|
|
498
|
+
function failureStatus(error) {
|
|
499
|
+
return error instanceof TrevRpcError
|
|
500
|
+
? error
|
|
501
|
+
: statusError(Code.Internal, error?.message ?? String(error), {}, error);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Validates a call timeout and returns the request field for it. Zero means unset on the wire, so
|
|
506
|
+
* a zero timeout is sent as 1ns.
|
|
507
|
+
*/
|
|
508
|
+
function timeoutField({ timeoutMs }) {
|
|
509
|
+
if (timeoutMs == null) {
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
512
|
+
if (!(timeoutMs >= 0 && Number.isFinite(timeoutMs))) {
|
|
513
|
+
throw invalidArgument("RPC timeout must be a non-negative finite number of milliseconds");
|
|
514
|
+
}
|
|
515
|
+
const nanos = BigInt(Math.ceil(timeoutMs)) * 1_000_000n;
|
|
516
|
+
if (nanos > MaxUint64) {
|
|
517
|
+
throw invalidArgument("RPC timeout does not fit in 64-bit nanoseconds");
|
|
518
|
+
}
|
|
519
|
+
return { timeoutNanos: String(nanos === 0n ? 1n : nanos) };
|
|
520
|
+
}
|