@trevrpc/trevrpc-js 0.2.2 → 1.0.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.
Files changed (42) hide show
  1. package/README.md +89 -47
  2. package/bin/protoc-gen-trevrpc-js.js +1 -1
  3. package/package.json +11 -41
  4. package/src/bytes.js +10 -0
  5. package/src/call.js +287 -0
  6. package/src/channel.js +131 -296
  7. package/src/client.js +203 -1292
  8. package/src/framing.js +125 -519
  9. package/src/generator.js +26 -103
  10. package/src/index.d.ts +79 -452
  11. package/src/index.js +4 -3
  12. package/src/status.js +24 -70
  13. package/src/wire.js +4 -59
  14. package/examples/greeter/README.md +0 -35
  15. package/examples/greeter/client.js +0 -172
  16. package/examples/greeter/greeter.node.trevrpc.d.ts +0 -53
  17. package/examples/greeter/greeter.node.trevrpc.js +0 -9
  18. package/examples/greeter/greeter.proto +0 -18
  19. package/examples/greeter/greeter.trevrpc.d.ts +0 -55
  20. package/examples/greeter/greeter.trevrpc.js +0 -103
  21. package/examples/greeter/index.html +0 -140
  22. package/examples/greeter/static-server.js +0 -168
  23. package/src/advanced.d.ts +0 -29
  24. package/src/advanced.js +0 -1
  25. package/src/browser-channel.js +0 -49
  26. package/src/connect.js +0 -6
  27. package/src/connect.node.js +0 -6
  28. package/src/index.node.d.ts +0 -10
  29. package/src/index.node.js +0 -3
  30. package/src/native-loader.js +0 -118
  31. package/src/node-advanced.d.ts +0 -27
  32. package/src/node-advanced.js +0 -1
  33. package/src/node-generated.d.ts +0 -24
  34. package/src/node-generated.js +0 -126
  35. package/src/node-index.d.ts +0 -1
  36. package/src/node-index.js +0 -7
  37. package/src/node.d.ts +0 -298
  38. package/src/node.js +0 -1456
  39. package/src/runtime.js +0 -66
  40. package/src/server-response.js +0 -49
  41. package/src/timer.js +0 -29
  42. package/src/webtransport.js +0 -639
@@ -1,639 +0,0 @@
1
- import { RequestWriterSettlement } from "./client.js";
2
- import {
3
- DefaultMaxFrameSize,
4
- FrameReader,
5
- writeFrame,
6
- writeMessageStreamFrames,
7
- } from "./framing.js";
8
- import { Code, TrevRpcError, cancelled, statusFromTransportError, unavailable } from "./status.js";
9
- import { RpcRequest, RpcResponse, RpcStreamFrameKind } from "./wire.js";
10
-
11
- const CancelledStreamReason = new DOMException("TrevRPC stream cancelled", "AbortError");
12
- const RequestBodyBatchMaxMessages = 64;
13
- const RequestBodyBatchMaxBytes = 64 * 1024;
14
- const ResponseFrameBatchMaxMessages = 64;
15
- const BrowserWebTransportOptionKeys = [
16
- "allowPooling",
17
- "congestionControl",
18
- "requireUnreliable",
19
- "serverCertificateHashes",
20
- ];
21
-
22
- /** Transport implementation for TrevRPC over WebTransport. */
23
- export class RawWebTransport {
24
- /** Creates a client over an established WebTransport session. */
25
- constructor(session, options = {}) {
26
- this.session = session;
27
- this.maxFrameSize = options.maxFrameSize ?? DefaultMaxFrameSize;
28
- }
29
-
30
- /** Opens a WebTransport session and wraps it in a TrevRPC client. */
31
- static async connect(url, options = {}) {
32
- const session = createWebTransportSession(url, options);
33
- await waitForWebTransportReady(session);
34
- return new RawWebTransport(session, options);
35
- }
36
-
37
- /** Waits for the underlying WebTransport session to become ready. */
38
- async ready() {
39
- await waitForWebTransportReady(this.session);
40
- }
41
-
42
- /** Closes the underlying WebTransport session. */
43
- close(closeInfo = {}) {
44
- if (typeof this.session.close === "function") {
45
- this.session.close(closeInfo);
46
- }
47
- }
48
-
49
- /** Sends a unary RPC request over WebTransport and returns its response. */
50
- async call(request, options = {}) {
51
- let writer;
52
- let reader;
53
- let complete = false;
54
- let cleanupAbort = () => {};
55
- let writerReleaseInCloseTask = false;
56
-
57
- try {
58
- throwIfAborted(options.signal);
59
- await abortable(this.ready(), options.signal);
60
- const stream = await abortableBidirectionalStream(
61
- this.openBidirectionalStream(),
62
- options.signal,
63
- );
64
- writer = stream.writable.getWriter();
65
- reader = stream.readable.getReader();
66
- cleanupAbort = onAbort(options.signal, () => {
67
- void abortWriter(writer);
68
- void cancelReader(reader);
69
- });
70
- const frameReader = new FrameReader(reader);
71
-
72
- await abortable(
73
- writeFrame(writer, RpcRequest, request, options.maxFrameSize ?? this.maxFrameSize),
74
- options.signal,
75
- );
76
- closeUnaryRequestWriter(writer);
77
- writerReleaseInCloseTask = true;
78
- const response = await abortable(
79
- frameReader.readFrame(RpcResponse, options.maxFrameSize ?? this.maxFrameSize),
80
- options.signal,
81
- );
82
- complete = true;
83
- return response;
84
- } catch (error) {
85
- throw statusFromTransportError(error);
86
- } finally {
87
- if (!complete) {
88
- await abortWriter(writer);
89
- await cancelReader(reader);
90
- }
91
- if (!writerReleaseInCloseTask) {
92
- releaseLock(writer);
93
- }
94
- releaseLock(reader);
95
- cleanupAbort();
96
- }
97
- }
98
-
99
- /** Sends a streaming RPC request over WebTransport and returns response frames. */
100
- async streamingCall(request, requestBody, options = {}) {
101
- try {
102
- throwIfAborted(options.signal);
103
- await abortable(this.ready(), options.signal);
104
- const stream = await abortableBidirectionalStream(
105
- this.openBidirectionalStream(),
106
- options.signal,
107
- );
108
- const writer = stream.writable.getWriter();
109
- const reader = stream.readable.getReader();
110
- const maxFrameSize = options.maxFrameSize ?? this.maxFrameSize;
111
- const abortRequestWriter = onceAsync(() => abortWriter(writer));
112
- const writerSettlement = new RequestWriterSettlement(
113
- statusFromTransportError,
114
- isCleanupWriterError,
115
- isRemoteStopSendingWriterError,
116
- );
117
- writerSettlement.track(
118
- writeStreamingRequest(
119
- writer,
120
- request,
121
- requestBody,
122
- maxFrameSize,
123
- abortRequestWriter,
124
- (error) => writerSettlement.recordError(error),
125
- ),
126
- );
127
- return new WebTransportResponseFrameStream(
128
- reader,
129
- writerSettlement,
130
- abortRequestWriter,
131
- maxFrameSize,
132
- options.signal,
133
- );
134
- } catch (error) {
135
- throw statusFromTransportError(error);
136
- }
137
- }
138
-
139
- /** Opens a bidirectional WebTransport stream. */
140
- async openBidirectionalStream() {
141
- if (typeof this.session.createBidirectionalStream !== "function") {
142
- throw unavailable("WebTransport session does not support bidirectional streams");
143
- }
144
-
145
- return this.session.createBidirectionalStream();
146
- }
147
- }
148
-
149
- /** Waits for readiness without hanging when a session closes during setup. */
150
- export async function waitForWebTransportReady(session) {
151
- if (session.closed == null) {
152
- await session.ready;
153
- return;
154
- }
155
-
156
- await Promise.race([
157
- session.ready,
158
- Promise.resolve(session.closed).then(() => {
159
- throw unavailable("WebTransport session closed before becoming ready");
160
- }),
161
- ]);
162
- }
163
-
164
- /** Creates a WebTransport session using only browser constructor options. */
165
- export function createWebTransportSession(url, options = {}) {
166
- const WebTransportCtor = options.WebTransport ?? globalThis.WebTransport;
167
- if (typeof WebTransportCtor !== "function") {
168
- throw unavailable("WebTransport is not available in this JavaScript runtime");
169
- }
170
-
171
- return new WebTransportCtor(url, browserConstructorOptions(options));
172
- }
173
-
174
- function browserConstructorOptions(options) {
175
- const constructorOptions = {};
176
- for (const key of BrowserWebTransportOptionKeys) {
177
- if (Object.hasOwn(options, key) && options[key] !== undefined) {
178
- constructorOptions[key] = options[key];
179
- }
180
- }
181
-
182
- return constructorOptions;
183
- }
184
-
185
- class WebTransportResponseFrameStream {
186
- constructor(
187
- reader,
188
- writerSettlement,
189
- abortRequestWriter,
190
- maxFrameSize = DefaultMaxFrameSize,
191
- signal = undefined,
192
- ) {
193
- this.reader = reader;
194
- this.frameReader = new FrameReader(reader);
195
- this.abortRequestWriter = abortRequestWriter;
196
- this.maxFrameSize = maxFrameSize;
197
- this.readBatchMaxMessages = ResponseFrameBatchMaxMessages;
198
- this.frameQueue = [];
199
- this.frameQueueHead = 0;
200
- this.done = false;
201
- this.readerOwned = true;
202
- this.cleanupAbort = () => {};
203
- this.cleanupDone = null;
204
- this.writerSettlement = writerSettlement;
205
- this.cleanupAbort = onAbort(signal, () => this.startCleanup());
206
- }
207
-
208
- [Symbol.asyncIterator]() {
209
- return this;
210
- }
211
-
212
- async next() {
213
- if (this.done) {
214
- return await this.finishDone();
215
- }
216
-
217
- try {
218
- await this.fillFrameQueue();
219
- const frame = this.takeQueuedFrame();
220
- if (frame == null) {
221
- return await this.finishEof();
222
- }
223
-
224
- if (frame.kind === RpcStreamFrameKind.Status) {
225
- await this.finishStatus(frame);
226
- }
227
-
228
- return { done: false, value: frame };
229
- } catch (error) {
230
- this.startCleanup();
231
- throw statusFromTransportError(error);
232
- }
233
- }
234
-
235
- async nextBatch(max = this.readBatchMaxMessages) {
236
- if (this.done) {
237
- return await this.finishDone();
238
- }
239
-
240
- try {
241
- await this.fillFrameQueue(max);
242
- const terminalOffset = this.findQueuedTerminalOffset();
243
- if (terminalOffset === 0) {
244
- const frame = this.takeQueuedFrame();
245
- if (frame == null) {
246
- return await this.finishEof();
247
- }
248
- await this.finishStatus(frame);
249
- return { done: false, value: [frame] };
250
- }
251
-
252
- const count = terminalOffset < 0 ? this.queuedFrameCount() : terminalOffset;
253
- return { done: false, value: this.takeQueuedFrames(count) };
254
- } catch (error) {
255
- this.startCleanup();
256
- throw statusFromTransportError(error);
257
- }
258
- }
259
-
260
- async nextBodyBatch(max = this.readBatchMaxMessages) {
261
- if (this.done) {
262
- return await this.finishDone();
263
- }
264
-
265
- try {
266
- const batch = await this.frameReader.readStreamMessageBodyBatchOrEOF(max, this.maxFrameSize);
267
- if (batch == null) {
268
- return await this.finishEof();
269
- }
270
- if (batch.status != null) {
271
- await this.finishStatus(batch.status);
272
- }
273
- return { done: false, value: batch };
274
- } catch (error) {
275
- this.startCleanup();
276
- throw statusFromTransportError(error);
277
- }
278
- }
279
-
280
- async return() {
281
- this.startCleanup();
282
- return await this.finishDone();
283
- }
284
-
285
- startCleanup() {
286
- this.writerSettlement.beginCleanup();
287
- if (this.cleanupDone != null) {
288
- return;
289
- }
290
-
291
- this.done = true;
292
- this.clearFrameQueue();
293
- this.cleanupAbort();
294
- const cancel = this.readerOwned ? cancelReader(this.reader) : Promise.resolve();
295
- this.cleanupDone = Promise.all([cancel, this.abortRequestWriter()]).then(() => {
296
- this.releaseReader();
297
- });
298
- }
299
-
300
- releaseReader() {
301
- if (!this.readerOwned) {
302
- return;
303
- }
304
- this.readerOwned = false;
305
- releaseLock(this.reader);
306
- }
307
-
308
- async fillFrameQueue(max = this.readBatchMaxMessages) {
309
- if (this.queuedFrameCount() > 0) {
310
- return;
311
- }
312
-
313
- const batch = await this.frameReader.readStreamFrameBatchOrEOF(max, this.maxFrameSize);
314
- if (batch == null) {
315
- this.frameQueue = [null];
316
- } else {
317
- this.frameQueue = batch;
318
- }
319
- this.frameQueueHead = 0;
320
- }
321
-
322
- async finishEof() {
323
- this.startCleanup();
324
- return await this.finishDone();
325
- }
326
-
327
- async finishDone() {
328
- await this.cleanupDone;
329
- await this.writerSettlement.finish();
330
- return { done: true, value: undefined };
331
- }
332
-
333
- finishStatus(frame) {
334
- this.writerSettlement.recordTerminal(frame.status ?? Code.Ok);
335
- }
336
-
337
- queuedFrameCount() {
338
- return this.frameQueue.length - this.frameQueueHead;
339
- }
340
-
341
- takeQueuedFrame() {
342
- const frame = this.frameQueue[this.frameQueueHead];
343
- this.frameQueueHead += 1;
344
- this.compactFrameQueue();
345
- return frame;
346
- }
347
-
348
- takeQueuedFrames(count) {
349
- if (count <= 0) {
350
- return [];
351
- }
352
- const start = this.frameQueueHead;
353
- const end = start + count;
354
- if (start === 0 && end === this.frameQueue.length) {
355
- const frames = this.frameQueue;
356
- this.frameQueue = [];
357
- this.frameQueueHead = 0;
358
- return frames;
359
- }
360
-
361
- const frames = this.frameQueue.slice(start, end);
362
- this.frameQueueHead = end;
363
- this.compactFrameQueue();
364
- return frames;
365
- }
366
-
367
- findQueuedTerminalOffset() {
368
- for (let index = this.frameQueueHead; index < this.frameQueue.length; index += 1) {
369
- if (isTerminalFrame(this.frameQueue[index])) {
370
- return index - this.frameQueueHead;
371
- }
372
- }
373
- return -1;
374
- }
375
-
376
- clearFrameQueue() {
377
- this.frameQueue.length = 0;
378
- this.frameQueueHead = 0;
379
- }
380
-
381
- compactFrameQueue() {
382
- if (this.frameQueueHead === 0) {
383
- return;
384
- }
385
- if (this.frameQueueHead === this.frameQueue.length) {
386
- this.clearFrameQueue();
387
- return;
388
- }
389
- if (this.frameQueueHead >= 32 && this.frameQueueHead * 2 >= this.frameQueue.length) {
390
- this.frameQueue.splice(0, this.frameQueueHead);
391
- this.frameQueueHead = 0;
392
- }
393
- }
394
- }
395
-
396
- function isTerminalFrame(frame) {
397
- return frame == null || frame.kind === RpcStreamFrameKind.Status;
398
- }
399
-
400
- function isCleanupWriterError(error) {
401
- if (error?.code === Code.Cancelled) {
402
- return true;
403
- }
404
-
405
- const message = error?.statusMessage ?? error?.message ?? "";
406
- return error?.code === Code.Unavailable && /stream canceled with error code 0/i.test(message);
407
- }
408
-
409
- function isRemoteStopSendingWriterError(error) {
410
- const message = error?.statusMessage ?? error?.message ?? "";
411
- return error?.code === Code.Unavailable && /received stop_sending/i.test(message);
412
- }
413
-
414
- function closeUnaryRequestWriter(writer) {
415
- void (async () => {
416
- try {
417
- await writer.close();
418
- } catch {
419
- // Unary reads do not require waiting for the browser's upload close
420
- // acknowledgement. Response validation still reports server-side status.
421
- } finally {
422
- releaseLock(writer);
423
- }
424
- })();
425
- }
426
-
427
- function throwIfAborted(signal) {
428
- if (signal?.aborted) {
429
- throw signalAbortError(signal);
430
- }
431
- }
432
-
433
- function signalAbortError(signal) {
434
- const reason = signal?.reason;
435
- if (reason instanceof TrevRpcError) {
436
- return reason;
437
- }
438
- const error = cancelled("RPC cancelled");
439
- if (reason !== undefined) {
440
- Object.defineProperty(error, "cause", { configurable: true, value: reason });
441
- }
442
- return error;
443
- }
444
-
445
- function abortable(promise, signal) {
446
- throwIfAborted(signal);
447
- if (signal == null) {
448
- return promise;
449
- }
450
-
451
- return new Promise((resolve, reject) => {
452
- const onAbort = () => reject(signalAbortError(signal));
453
- signal.addEventListener("abort", onAbort, { once: true });
454
- promise.then(
455
- (value) => {
456
- signal.removeEventListener("abort", onAbort);
457
- resolve(value);
458
- },
459
- (error) => {
460
- signal.removeEventListener("abort", onAbort);
461
- reject(error);
462
- },
463
- );
464
- });
465
- }
466
-
467
- function abortableBidirectionalStream(promise, signal) {
468
- throwIfAborted(signal);
469
- if (signal == null) {
470
- return promise;
471
- }
472
-
473
- let aborted = false;
474
- return new Promise((resolve, reject) => {
475
- const onAbort = () => {
476
- aborted = true;
477
- reject(signalAbortError(signal));
478
- };
479
- signal.addEventListener("abort", onAbort, { once: true });
480
- promise.then(
481
- (stream) => {
482
- signal.removeEventListener("abort", onAbort);
483
- if (aborted || signal.aborted) {
484
- void cleanupBidirectionalStream(stream);
485
- reject(signalAbortError(signal));
486
- return;
487
- }
488
-
489
- resolve(stream);
490
- },
491
- (error) => {
492
- signal.removeEventListener("abort", onAbort);
493
- reject(error);
494
- },
495
- );
496
- });
497
- }
498
-
499
- function onceAsync(callback) {
500
- let task;
501
- return () => {
502
- if (task == null) {
503
- try {
504
- task = Promise.resolve(callback());
505
- } catch (error) {
506
- task = Promise.reject(error);
507
- }
508
- task.catch(() => {});
509
- }
510
- return task;
511
- };
512
- }
513
-
514
- function onAbort(signal, abort) {
515
- if (signal == null) {
516
- return () => {};
517
- }
518
-
519
- if (signal.aborted) {
520
- abort();
521
- return () => {};
522
- }
523
-
524
- signal.addEventListener("abort", abort, { once: true });
525
- return () => signal.removeEventListener("abort", abort);
526
- }
527
-
528
- async function writeStreamingRequest(
529
- writer,
530
- request,
531
- requestBody,
532
- maxFrameSize,
533
- abortRequestWriter,
534
- recordError,
535
- ) {
536
- let iterator;
537
- try {
538
- iterator = requestBody[Symbol.asyncIterator]();
539
- await writeFrame(writer, RpcRequest, request, maxFrameSize);
540
- for (;;) {
541
- const result = await nextRequestBodyBatch(iterator, RequestBodyBatchMaxMessages);
542
- if (result.done) {
543
- break;
544
- }
545
- await writeBodyBatches(writer, result.value, maxFrameSize, RequestBodyBatchMaxBytes);
546
- }
547
- await writer.close();
548
- } catch (error) {
549
- recordError(error);
550
- try {
551
- await iterator?.return?.();
552
- } catch {
553
- // Preserve the upload error; aborting the writer releases stream resources.
554
- }
555
- await abortRequestWriter();
556
- throw error;
557
- } finally {
558
- releaseLock(writer);
559
- }
560
- }
561
-
562
- async function nextRequestBodyBatch(iterator, maxMessages) {
563
- if (typeof iterator.nextBatch === "function") {
564
- return await iterator.nextBatch(maxMessages);
565
- }
566
- const result = await iterator.next();
567
- return result.done ? result : { done: false, value: [result.value] };
568
- }
569
-
570
- async function writeBodyBatches(writer, bodies, maxFrameSize, maxBytes) {
571
- const limit = Math.max(0, maxBytes ?? 0);
572
- if (limit === 0) {
573
- await writeMessageStreamFrames(writer, bodies, maxFrameSize);
574
- return;
575
- }
576
-
577
- let start = 0;
578
- let bytes = 0;
579
- for (let index = 0; index < bodies.length; index += 1) {
580
- const bodyBytes = bodies[index]?.byteLength ?? 0;
581
- if (index > start && bytes + bodyBytes > limit) {
582
- await writeMessageStreamFrames(writer, bodies, maxFrameSize, start, index);
583
- start = index;
584
- bytes = 0;
585
- }
586
- bytes += bodyBytes;
587
- }
588
- if (start < bodies.length) {
589
- await writeMessageStreamFrames(writer, bodies, maxFrameSize, start, bodies.length);
590
- }
591
- }
592
-
593
- async function cancelReader(reader) {
594
- if (reader != null && typeof reader.cancel === "function") {
595
- try {
596
- await reader.cancel(CancelledStreamReason);
597
- } catch {
598
- // The stream may already be closed or reset by the peer.
599
- }
600
- }
601
- }
602
-
603
- async function abortWriter(writer) {
604
- if (writer != null && typeof writer.abort === "function") {
605
- try {
606
- await writer.abort(CancelledStreamReason);
607
- } catch {
608
- // The stream may already be closed or reset by the peer.
609
- }
610
- }
611
- }
612
-
613
- async function cleanupBidirectionalStream(stream) {
614
- let writer;
615
- let reader;
616
- try {
617
- writer = stream?.writable?.getWriter?.();
618
- await abortWriter(writer);
619
- } finally {
620
- releaseLock(writer);
621
- }
622
-
623
- try {
624
- reader = stream?.readable?.getReader?.();
625
- await cancelReader(reader);
626
- } finally {
627
- releaseLock(reader);
628
- }
629
- }
630
-
631
- function releaseLock(lock) {
632
- if (lock != null && typeof lock.releaseLock === "function") {
633
- try {
634
- lock.releaseLock();
635
- } catch {
636
- // Releasing an already-released lock is harmless for cleanup purposes.
637
- }
638
- }
639
- }