@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.
Files changed (47) hide show
  1. package/README.md +127 -46
  2. package/package.json +12 -45
  3. package/src/bytes.js +10 -0
  4. package/src/call.js +520 -0
  5. package/src/channel.js +258 -288
  6. package/src/client.js +543 -1254
  7. package/src/datagram.js +336 -0
  8. package/src/framing.js +151 -548
  9. package/src/generator.js +66 -109
  10. package/src/index.d.ts +235 -463
  11. package/src/index.js +4 -3
  12. package/src/qmux/frames.js +262 -0
  13. package/src/qmux/params.js +104 -0
  14. package/src/qmux/session.js +739 -0
  15. package/src/race.js +90 -0
  16. package/src/status.js +24 -70
  17. package/src/varint.js +73 -0
  18. package/src/wire.js +21 -89
  19. package/examples/greeter/README.md +0 -35
  20. package/examples/greeter/client.js +0 -172
  21. package/examples/greeter/greeter.node.trevrpc.d.ts +0 -53
  22. package/examples/greeter/greeter.node.trevrpc.js +0 -9
  23. package/examples/greeter/greeter.proto +0 -18
  24. package/examples/greeter/greeter.trevrpc.d.ts +0 -55
  25. package/examples/greeter/greeter.trevrpc.js +0 -103
  26. package/examples/greeter/index.html +0 -140
  27. package/examples/greeter/static-server.js +0 -168
  28. package/src/advanced.d.ts +0 -29
  29. package/src/advanced.js +0 -1
  30. package/src/browser-channel.js +0 -49
  31. package/src/connect.js +0 -6
  32. package/src/connect.node.js +0 -6
  33. package/src/index.node.d.ts +0 -10
  34. package/src/index.node.js +0 -3
  35. package/src/native-loader.js +0 -118
  36. package/src/node-advanced.d.ts +0 -27
  37. package/src/node-advanced.js +0 -1
  38. package/src/node-generated.d.ts +0 -24
  39. package/src/node-generated.js +0 -126
  40. package/src/node-index.d.ts +0 -1
  41. package/src/node-index.js +0 -7
  42. package/src/node.d.ts +0 -298
  43. package/src/node.js +0 -1456
  44. package/src/runtime.js +0 -66
  45. package/src/server-response.js +0 -49
  46. package/src/timer.js +0 -29
  47. package/src/webtransport.js +0 -639
@@ -0,0 +1,336 @@
1
+ // Unreliable RPC messages over WebTransport datagrams. One DatagramEndpoint per session owns the
2
+ // session's single datagram reader loop, its single shared writer, the flow ID allocator, and the
3
+ // demultiplexer from flow IDs to calls. Calls that stream an unreliable direction open a
4
+ // DatagramFlow on the endpoint of the session that carries them. QMux over WebSocket has no
5
+ // datagrams, so those calls never get a flow and everything stays on the stream.
6
+ import { byteView } from "./bytes.js";
7
+ import { readVarint, varintLength, writeVarint } from "./varint.js";
8
+
9
+ /**
10
+ * Largest datagram assumed when the browser does not report `maxDatagramSize`. Browsers silently
11
+ * drop datagrams above the real limit, so this stays below a typical path MTU.
12
+ */
13
+ export const FallbackMaxDatagramSize = 1100;
14
+
15
+ /**
16
+ * How much a flow buffers for a call that is not ready to take datagrams yet: at most this many
17
+ * messages or bytes, then further datagrams are dropped.
18
+ */
19
+ export const FlowQueueMessages = 32;
20
+ export const FlowQueueBytes = 1 << 20;
21
+
22
+ const MessageFrameType = 0;
23
+
24
+ /** Encoded size of one message as a MESSAGE frame inside a datagram. */
25
+ export function messageFrameSize(message) {
26
+ return 1 + varintLength(message.byteLength) + message.byteLength;
27
+ }
28
+
29
+ /**
30
+ * Encodes a datagram payload: the flow ID as a QUIC varint, then one MESSAGE frame per message,
31
+ * in order.
32
+ */
33
+ export function encodeDatagram(flow, messages) {
34
+ let size = varintLength(flow);
35
+ for (const message of messages) {
36
+ size += messageFrameSize(message);
37
+ }
38
+ const datagram = new Uint8Array(size);
39
+ let offset = writeVarint(datagram, 0, flow);
40
+ for (const message of messages) {
41
+ datagram[offset] = MessageFrameType;
42
+ offset = writeVarint(datagram, offset + 1, message.byteLength);
43
+ datagram.set(message, offset);
44
+ offset += message.byteLength;
45
+ }
46
+ return datagram;
47
+ }
48
+
49
+ /**
50
+ * Decodes a datagram payload as `{ flow, messages }`, or null if it has no complete flow ID. The
51
+ * messages are the payloads of its MESSAGE frames; every other frame is skipped. A datagram whose
52
+ * frames do not exactly fill it decodes to no messages at all.
53
+ */
54
+ export function decodeDatagram(datagram) {
55
+ const bytes = byteView(datagram);
56
+ const flow = readVarint(bytes, 0);
57
+ if (flow === null) {
58
+ return null;
59
+ }
60
+ const messages = [];
61
+ let offset = flow.next;
62
+ while (offset < bytes.byteLength) {
63
+ const type = readVarint(bytes, offset);
64
+ const length = type === null ? null : readVarint(bytes, type.next);
65
+ if (length === null || typeof length.value !== "number") {
66
+ return { flow: flow.value, messages: [] };
67
+ }
68
+ const end = length.next + length.value;
69
+ if (end > bytes.byteLength) {
70
+ return { flow: flow.value, messages: [] };
71
+ }
72
+ if (type.value === MessageFrameType) {
73
+ messages.push(bytes.subarray(length.next, end));
74
+ }
75
+ offset = end;
76
+ }
77
+ return { flow: flow.value, messages };
78
+ }
79
+
80
+ const endpoints = new WeakMap();
81
+
82
+ /**
83
+ * The shared endpoint of a session's `datagrams` object, or undefined if the session has none or
84
+ * its datagram reader is unusable. The endpoint lives as long as the object does.
85
+ */
86
+ export function datagramEndpoint(datagrams) {
87
+ if (datagrams == null || typeof datagrams !== "object") {
88
+ return undefined;
89
+ }
90
+ let endpoint = endpoints.get(datagrams);
91
+ if (endpoint === undefined) {
92
+ endpoint = new DatagramEndpoint(datagrams);
93
+ endpoints.set(datagrams, endpoint);
94
+ }
95
+ return endpoint.usable ? endpoint : undefined;
96
+ }
97
+
98
+ class DatagramEndpoint {
99
+ #datagrams;
100
+ /** Flows that accept datagrams, by ID. */
101
+ #flows = new Map();
102
+ #nextFlow = 1;
103
+ #reading = false;
104
+ #closed;
105
+ /** The shared writer: undefined until first needed, null once unavailable. */
106
+ #writer;
107
+
108
+ constructor(datagrams) {
109
+ this.#datagrams = datagrams;
110
+ this.#closed = typeof datagrams.readable?.getReader !== "function";
111
+ }
112
+
113
+ get usable() {
114
+ return !this.#closed;
115
+ }
116
+
117
+ /**
118
+ * Allocates the next flow of this session. A flow that `receive`s is registered with the
119
+ * reader, so datagrams for its ID reach it. Returns undefined if the session cannot read
120
+ * datagrams.
121
+ */
122
+ openFlow({ receive }) {
123
+ if (!this.#start()) {
124
+ return undefined;
125
+ }
126
+ const flow = new DatagramFlow(this, this.#nextFlow);
127
+ this.#nextFlow += 1;
128
+ if (receive) {
129
+ this.#flows.set(flow.id, flow);
130
+ }
131
+ return flow;
132
+ }
133
+
134
+ /** Stops routing datagrams to a flow; its ID is never reused. */
135
+ closeFlow(id) {
136
+ this.#flows.delete(id);
137
+ }
138
+
139
+ /**
140
+ * How many bytes of MESSAGE frames fit in a datagram of `flow` right now, or 0 if this session
141
+ * cannot send datagrams.
142
+ */
143
+ frameRoom(flow) {
144
+ return this.#writerOrNull() === null
145
+ ? 0
146
+ : Math.max(0, this.maxDatagramSize - varintLength(flow));
147
+ }
148
+
149
+ /**
150
+ * Sends messages as one datagram of `flow`. Resolves true once the datagram was handed to the
151
+ * transport, and false if the messages have to go on the stream instead: they do not fit, there
152
+ * is no writer, or the write failed. Waits for send-buffer space until `signal` aborts, then
153
+ * rejects with the abort reason.
154
+ */
155
+ async send(flow, messages, signal) {
156
+ const writer = this.#writerOrNull();
157
+ const datagram = writer === null ? null : encodeDatagram(flow, messages);
158
+ if (datagram === null || datagram.byteLength > this.maxDatagramSize) {
159
+ return false;
160
+ }
161
+ try {
162
+ if (!(writer.desiredSize > 0)) {
163
+ await abortable(writer.ready, signal);
164
+ }
165
+ await abortable(writer.write(datagram), signal);
166
+ return true;
167
+ } catch {
168
+ if (signal.aborted) {
169
+ throw signal.reason;
170
+ }
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /** The largest datagram payload the session accepts right now, flow ID included. */
176
+ get maxDatagramSize() {
177
+ const size = this.#datagrams.maxDatagramSize;
178
+ return Number.isFinite(size) && size > 0 ? size : FallbackMaxDatagramSize;
179
+ }
180
+
181
+ #start() {
182
+ if (this.#closed) {
183
+ return false;
184
+ }
185
+ if (!this.#reading) {
186
+ let reader;
187
+ try {
188
+ reader = this.#datagrams.readable.getReader();
189
+ } catch {
190
+ this.#closed = true;
191
+ return false;
192
+ }
193
+ this.#reading = true;
194
+ void this.#read(reader);
195
+ }
196
+ return true;
197
+ }
198
+
199
+ /** The reader loop. It never blocks on a call: delivery is a synchronous, bounded push. */
200
+ async #read(reader) {
201
+ try {
202
+ for (;;) {
203
+ const { value, done } = await reader.read();
204
+ if (done) {
205
+ break;
206
+ }
207
+ const datagram = decodeDatagram(value);
208
+ const flow = datagram === null ? undefined : this.#flows.get(datagram.flow);
209
+ // Unknown and ended flows drop the datagram, and so do malformed ones (no messages).
210
+ if (flow !== undefined) {
211
+ for (const message of datagram.messages) {
212
+ flow.deliver(message);
213
+ }
214
+ }
215
+ }
216
+ } catch {
217
+ // A failed session ends its calls through their streams.
218
+ } finally {
219
+ this.#closed = true;
220
+ this.#flows.clear();
221
+ }
222
+ }
223
+
224
+ #writerOrNull() {
225
+ if (this.#writer === undefined) {
226
+ this.#writer = this.#openWriter();
227
+ }
228
+ return this.#writer;
229
+ }
230
+
231
+ /** Locks the session's datagram writable once; a lost writer disables sending datagrams. */
232
+ #openWriter() {
233
+ try {
234
+ const datagrams = this.#datagrams;
235
+ const writable =
236
+ typeof datagrams.createWritable === "function"
237
+ ? datagrams.createWritable()
238
+ : datagrams.writable;
239
+ const writer = writable.getWriter();
240
+ writer.closed.catch(() => {
241
+ this.#writer = null;
242
+ });
243
+ return writer;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * One call's datagram flow. Datagrams for the flow wait in a bounded buffer until the call
252
+ * connects, then go straight to its handler. The call releases the flow when it ends.
253
+ */
254
+ export class DatagramFlow {
255
+ #endpoint;
256
+ #handler;
257
+ #buffered = [];
258
+ #bufferedBytes = 0;
259
+ #released = false;
260
+
261
+ constructor(endpoint, id) {
262
+ this.#endpoint = endpoint;
263
+ this.id = id;
264
+ }
265
+
266
+ /**
267
+ * Delivers one received message. It must not block or throw. Without a handler it is buffered,
268
+ * and dropped when the buffer is full.
269
+ */
270
+ deliver(message) {
271
+ if (this.#handler !== undefined) {
272
+ this.#handler(message);
273
+ } else if (
274
+ this.#buffered.length < FlowQueueMessages &&
275
+ this.#bufferedBytes + message.byteLength <= FlowQueueBytes
276
+ ) {
277
+ this.#buffered.push(message);
278
+ this.#bufferedBytes += message.byteLength;
279
+ }
280
+ }
281
+
282
+ /** Hands the buffered messages, then all later ones, to `handler`. Only the first call counts. */
283
+ connect(handler) {
284
+ if (this.#handler !== undefined || this.#released) {
285
+ return;
286
+ }
287
+ this.#handler = handler;
288
+ const buffered = this.#buffered;
289
+ this.#buffered = [];
290
+ this.#bufferedBytes = 0;
291
+ for (const message of buffered) {
292
+ handler(message);
293
+ }
294
+ }
295
+
296
+ /** See DatagramEndpoint.frameRoom; 0 once the flow is released. */
297
+ get frameRoom() {
298
+ return this.#released ? 0 : this.#endpoint.frameRoom(this.id);
299
+ }
300
+
301
+ /** See DatagramEndpoint.send. */
302
+ send(messages, signal) {
303
+ return this.#released ? Promise.resolve(false) : this.#endpoint.send(this.id, messages, signal);
304
+ }
305
+
306
+ /** Ends the flow: later datagrams for it are dropped. */
307
+ release() {
308
+ if (!this.#released) {
309
+ this.#released = true;
310
+ this.#handler = undefined;
311
+ this.#buffered = [];
312
+ this.#endpoint.closeFlow(this.id);
313
+ }
314
+ }
315
+ }
316
+
317
+ /** Settles like `promise`, or rejects with the abort reason as soon as `signal` aborts. */
318
+ function abortable(promise, signal) {
319
+ if (signal.aborted) {
320
+ return Promise.reject(signal.reason);
321
+ }
322
+ return new Promise((resolve, reject) => {
323
+ const onAbort = () => reject(signal.reason);
324
+ signal.addEventListener("abort", onAbort, { once: true });
325
+ promise.then(
326
+ (value) => {
327
+ signal.removeEventListener("abort", onAbort);
328
+ resolve(value);
329
+ },
330
+ (error) => {
331
+ signal.removeEventListener("abort", onAbort);
332
+ reject(error);
333
+ },
334
+ );
335
+ });
336
+ }