@trevrpc/trevrpc-js 0.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/bin/protoc-gen-trevrpc-js.js +17 -0
  4. package/examples/greeter/README.md +35 -0
  5. package/examples/greeter/client.js +172 -0
  6. package/examples/greeter/greeter.node.trevrpc.d.ts +53 -0
  7. package/examples/greeter/greeter.node.trevrpc.js +9 -0
  8. package/examples/greeter/greeter.proto +18 -0
  9. package/examples/greeter/greeter.trevrpc.d.ts +55 -0
  10. package/examples/greeter/greeter.trevrpc.js +103 -0
  11. package/examples/greeter/index.html +140 -0
  12. package/examples/greeter/static-server.js +168 -0
  13. package/package.json +89 -0
  14. package/src/advanced.d.ts +29 -0
  15. package/src/advanced.js +1 -0
  16. package/src/browser-channel.js +45 -0
  17. package/src/channel.js +375 -0
  18. package/src/client.js +1397 -0
  19. package/src/connect.js +6 -0
  20. package/src/connect.node.js +6 -0
  21. package/src/framing.js +618 -0
  22. package/src/generator.js +868 -0
  23. package/src/index.d.ts +577 -0
  24. package/src/index.js +3 -0
  25. package/src/index.node.d.ts +10 -0
  26. package/src/index.node.js +3 -0
  27. package/src/metadata.js +112 -0
  28. package/src/native-loader.js +94 -0
  29. package/src/node-advanced.d.ts +27 -0
  30. package/src/node-advanced.js +1 -0
  31. package/src/node-generated.d.ts +24 -0
  32. package/src/node-generated.js +126 -0
  33. package/src/node-index.d.ts +1 -0
  34. package/src/node-index.js +7 -0
  35. package/src/node.d.ts +298 -0
  36. package/src/node.js +1456 -0
  37. package/src/runtime.js +66 -0
  38. package/src/server-response.js +49 -0
  39. package/src/status.js +145 -0
  40. package/src/timer.js +29 -0
  41. package/src/webtransport.js +624 -0
  42. package/src/wire.js +403 -0
package/src/channel.js ADDED
@@ -0,0 +1,375 @@
1
+ import { cancelled, deadlineExceeded, unavailable } from "./status.js";
2
+
3
+ const ChannelOptionKeys = new Set(["onStateChange", "signal", "timeoutMs"]);
4
+
5
+ const DefaultChannelOptions = Object.freeze({
6
+ initialDelayMs: 100,
7
+ maxDelayMs: 30_000,
8
+ multiplier: 2,
9
+ jitter: 0.2,
10
+ });
11
+ const MaxTimerDelayMs = 2_147_483_647;
12
+
13
+ /** Shared state machine for reconnecting channels. */
14
+ export class ChannelStateMachine extends EventTarget {
15
+ #connect;
16
+ #connectAbort = null;
17
+ #generation = 0;
18
+ #observeClosed;
19
+ #isConnectionFailure;
20
+ #onStateChange;
21
+ #options;
22
+ #state = "connecting";
23
+ #transport = null;
24
+ #waiters = [];
25
+
26
+ constructor(connect, options = {}, observeClosed = null, isConnectionFailure = () => false) {
27
+ super();
28
+ this.#connect = connect;
29
+ this.#observeClosed = observeClosed;
30
+ this.#isConnectionFailure = isConnectionFailure;
31
+ this.#onStateChange = options.onStateChange;
32
+ this.#options = normalizeChannelOptions(options);
33
+ queueMicrotask(() => this.#runConnectLoop(false));
34
+ }
35
+
36
+ /** Reports whether a generation is currently ready to accept new calls. */
37
+ get ready() {
38
+ return this.#state === "ready";
39
+ }
40
+
41
+ /** Reports the channel lifecycle state. */
42
+ get state() {
43
+ return this.#state;
44
+ }
45
+
46
+ /** Identifies the current successful connection generation, starting at one. */
47
+ get generation() {
48
+ return this.#generation;
49
+ }
50
+
51
+ /** Waits for the current or next connection generation to become ready. */
52
+ waitUntilReady() {
53
+ if (this.ready) {
54
+ return Promise.resolve();
55
+ }
56
+ if (this.#state === "closed") {
57
+ return Promise.reject(unavailable("channel is closed"));
58
+ }
59
+
60
+ return new Promise((resolve, reject) => {
61
+ this.#waiters.push({ resolve, reject });
62
+ });
63
+ }
64
+
65
+ /** Sends a unary call on the current generation without replaying it. */
66
+ async call(request, options = {}) {
67
+ const transport = this.#readyTransport();
68
+ try {
69
+ return await transport.call(request, options);
70
+ } catch (error) {
71
+ this.#operationFailed(transport, error);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ /** Starts a streaming call on the current generation without resuming it later. */
77
+ async streamingCall(request, requestBody, options = {}) {
78
+ const transport = this.#readyTransport();
79
+ try {
80
+ const stream = await transport.streamingCall(request, requestBody, options);
81
+ return observedStream(stream, (error) => this.#operationFailed(transport, error));
82
+ } catch (error) {
83
+ this.#operationFailed(transport, error);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /** Stops reconnecting and closes the current generation. */
89
+ close(closeInfo = {}) {
90
+ if (this.#state === "closed") {
91
+ return;
92
+ }
93
+
94
+ const transport = this.#transport;
95
+ this.#transport = null;
96
+ this.#connectAbort?.abort();
97
+ this.#connectAbort = null;
98
+ this.#transition("closed");
99
+ this.#rejectWaiters(unavailable("channel is closed"));
100
+ safeClose(transport, closeInfo);
101
+ }
102
+
103
+ #readyTransport() {
104
+ if (!this.ready || this.#transport == null) {
105
+ throw unavailable(`channel is ${this.#state}`);
106
+ }
107
+ return this.#transport;
108
+ }
109
+
110
+ async #runConnectLoop(reconnecting, initialError) {
111
+ if (this.#state === "closed" || this.#connectAbort != null) {
112
+ return;
113
+ }
114
+
115
+ const controller = new AbortController();
116
+ this.#connectAbort = controller;
117
+ let attempt = 0;
118
+ let error = initialError;
119
+
120
+ while (!controller.signal.aborted) {
121
+ attempt += 1;
122
+ const delayed = reconnecting || attempt > 1;
123
+ const delayMs = delayed
124
+ ? reconnectDelay(this.#options, reconnecting ? attempt - 1 : attempt - 2)
125
+ : 0;
126
+ this.#transition(reconnecting ? "reconnecting" : "connecting", {
127
+ attempt,
128
+ delayMs,
129
+ error,
130
+ });
131
+
132
+ if (delayed) {
133
+ await waitForDelay(delayMs, controller.signal);
134
+ if (controller.signal.aborted) {
135
+ return;
136
+ }
137
+ }
138
+
139
+ let transport;
140
+ try {
141
+ transport = await this.#connect(controller.signal);
142
+ } catch (connectError) {
143
+ if (controller.signal.aborted) {
144
+ return;
145
+ }
146
+ error = connectError;
147
+ continue;
148
+ }
149
+
150
+ if (controller.signal.aborted || this.#state === "closed") {
151
+ safeClose(transport);
152
+ return;
153
+ }
154
+
155
+ this.#connectAbort = null;
156
+ this.#transport = transport;
157
+ this.#generation += 1;
158
+ this.#transition("ready", { attempt });
159
+ this.#resolveWaiters();
160
+ this.#watchTransport(transport);
161
+ return;
162
+ }
163
+ }
164
+
165
+ #watchTransport(transport) {
166
+ const closed = this.#observeClosed?.(transport);
167
+ if (closed == null || typeof closed.then !== "function") {
168
+ return;
169
+ }
170
+
171
+ Promise.resolve(closed).then(
172
+ (closeInfo) => this.#transportFailed(transport, closeInfo),
173
+ (error) => this.#transportFailed(transport, error),
174
+ );
175
+ }
176
+
177
+ #transportFailed(transport, error) {
178
+ if (this.#state !== "ready" || transport !== this.#transport) {
179
+ return;
180
+ }
181
+
182
+ this.#transport = null;
183
+ safeClose(transport);
184
+ void this.#runConnectLoop(true, error);
185
+ }
186
+
187
+ #operationFailed(transport, error) {
188
+ if (this.#isConnectionFailure(error)) {
189
+ this.#transportFailed(transport, error);
190
+ }
191
+ }
192
+
193
+ #transition(state, extra = {}) {
194
+ const previousState = this.#state;
195
+ this.#state = state;
196
+ const detail = Object.freeze({
197
+ state,
198
+ previousState,
199
+ generation: this.#generation,
200
+ ...extra,
201
+ });
202
+
203
+ if (typeof this.#onStateChange === "function") {
204
+ try {
205
+ this.#onStateChange(detail);
206
+ } catch (error) {
207
+ globalThis.reportError?.(error);
208
+ }
209
+ }
210
+ this.dispatchEvent(lifecycleEvent("statechange", detail));
211
+ this.dispatchEvent(lifecycleEvent(state, detail));
212
+ }
213
+
214
+ #resolveWaiters() {
215
+ for (const waiter of this.#waiters.splice(0)) {
216
+ waiter.resolve();
217
+ }
218
+ }
219
+
220
+ #rejectWaiters(error) {
221
+ for (const waiter of this.#waiters.splice(0)) {
222
+ waiter.reject(error);
223
+ }
224
+ }
225
+ }
226
+
227
+ /** Returns transport options with channel lifecycle settings removed. */
228
+ export function stripChannelOptions(options = {}) {
229
+ return Object.fromEntries(Object.entries(options).filter(([key]) => !ChannelOptionKeys.has(key)));
230
+ }
231
+
232
+ /** Waits for initial readiness with caller-controlled cancellation and deadline. */
233
+ export async function waitForInitialReady(channel, options = {}) {
234
+ const timeoutMs = options.timeoutMs;
235
+ if (timeoutMs != null && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
236
+ channel.close();
237
+ throw new TypeError("timeoutMs must be a non-negative finite number");
238
+ }
239
+
240
+ const signal = options.signal;
241
+ if (signal?.aborted) {
242
+ channel.close();
243
+ throw initialAbortError(signal);
244
+ }
245
+ if (timeoutMs === 0) {
246
+ channel.close();
247
+ throw deadlineExceeded("initial connection deadline exceeded");
248
+ }
249
+
250
+ const timeoutAbort = timeoutMs == null ? null : new AbortController();
251
+ let removeAbort = () => {};
252
+ try {
253
+ const waits = [channel.waitUntilReady()];
254
+ if (signal != null) {
255
+ waits.push(
256
+ new Promise((_, reject) => {
257
+ const rejectCancelled = () => reject(initialAbortError(signal));
258
+ if (signal.aborted) {
259
+ rejectCancelled();
260
+ return;
261
+ }
262
+ signal.addEventListener("abort", rejectCancelled, { once: true });
263
+ removeAbort = () => signal.removeEventListener("abort", rejectCancelled);
264
+ }),
265
+ );
266
+ }
267
+ if (timeoutMs != null) {
268
+ waits.push(
269
+ waitForDelay(timeoutMs, timeoutAbort.signal).then(() => {
270
+ throw deadlineExceeded("initial connection deadline exceeded");
271
+ }),
272
+ );
273
+ }
274
+ await Promise.race(waits);
275
+ } catch (error) {
276
+ channel.close();
277
+ throw error;
278
+ } finally {
279
+ timeoutAbort?.abort();
280
+ removeAbort();
281
+ }
282
+ }
283
+
284
+ function normalizeChannelOptions(options) {
285
+ void options;
286
+ return DefaultChannelOptions;
287
+ }
288
+
289
+ function initialAbortError(signal) {
290
+ if (signal.reason?.name === "TrevRpcError") {
291
+ return signal.reason;
292
+ }
293
+ const error = cancelled("initial connection cancelled");
294
+ if (signal.reason !== undefined) {
295
+ Object.defineProperty(error, "cause", { configurable: true, value: signal.reason });
296
+ }
297
+ return error;
298
+ }
299
+
300
+ function reconnectDelay(options, exponent) {
301
+ const exponential = options.initialDelayMs * options.multiplier ** exponent;
302
+ const base = Math.min(options.maxDelayMs, exponential);
303
+ const jitter = 1 + (Math.random() * 2 - 1) * options.jitter;
304
+ return Math.min(options.maxDelayMs, Math.max(0, base * jitter));
305
+ }
306
+
307
+ async function waitForDelay(delayMs, signal) {
308
+ let remaining = delayMs;
309
+ while (remaining > 0 && !signal.aborted) {
310
+ const chunk = Math.min(remaining, MaxTimerDelayMs);
311
+ await waitForTimer(chunk, signal);
312
+ remaining -= chunk;
313
+ }
314
+ }
315
+
316
+ function waitForTimer(delayMs, signal) {
317
+ return new Promise((resolve) => {
318
+ const timer = setTimeout(done, delayMs);
319
+ signal.addEventListener("abort", done, { once: true });
320
+
321
+ function done() {
322
+ clearTimeout(timer);
323
+ signal.removeEventListener("abort", done);
324
+ resolve();
325
+ }
326
+ });
327
+ }
328
+
329
+ function lifecycleEvent(type, detail) {
330
+ const event = new Event(type);
331
+ Object.defineProperty(event, "detail", { value: detail });
332
+ return event;
333
+ }
334
+
335
+ function observedStream(stream, onFailure) {
336
+ const iterator = stream[Symbol.asyncIterator]();
337
+ const observed = {
338
+ [Symbol.asyncIterator]() {
339
+ return this;
340
+ },
341
+ next(...args) {
342
+ return observeResult(() => iterator.next(...args), onFailure);
343
+ },
344
+ };
345
+
346
+ for (const method of ["nextBatch", "nextBodyBatch"]) {
347
+ if (typeof iterator[method] === "function") {
348
+ observed[method] = (...args) => observeResult(() => iterator[method](...args), onFailure);
349
+ }
350
+ }
351
+ if (typeof iterator.return === "function") {
352
+ observed.return = (...args) => iterator.return(...args);
353
+ }
354
+ if (typeof iterator.throw === "function") {
355
+ observed.throw = (...args) => iterator.throw(...args);
356
+ }
357
+ return observed;
358
+ }
359
+
360
+ async function observeResult(operation, onFailure) {
361
+ try {
362
+ return await operation();
363
+ } catch (error) {
364
+ onFailure(error);
365
+ throw error;
366
+ }
367
+ }
368
+
369
+ function safeClose(transport, closeInfo) {
370
+ try {
371
+ transport?.close?.(closeInfo);
372
+ } catch {
373
+ // The failed generation may already have released its transport resources.
374
+ }
375
+ }