@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
package/src/channel.js CHANGED
@@ -1,375 +1,345 @@
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;
1
+ import { QMuxWebSocketSession } from "./qmux/session.js";
2
+ import { dialRace } from "./race.js";
3
+ import { cancelled, failedPrecondition, statusFromTransportError, unavailable } from "./status.js";
4
+
5
+ /**
6
+ * The WebTransport protocol that names wire version 1, offered through the `protocols` constructor
7
+ * option. It is the protocol version, so callers cannot change it.
8
+ */
9
+ const WebTransportProtocol = "trevrpc-1";
10
+
11
+ /** WebTransport constructor options passed through unchanged. */
12
+ const WebTransportOptionKeys = [
13
+ "allowPooling",
14
+ "congestionControl",
15
+ "requireUnreliable",
16
+ "serverCertificateHashes",
17
+ ];
18
+
19
+ const Backoff = Object.freeze({ initialDelayMs: 100, maxDelayMs: 30_000, jitter: 0.2 });
20
+
21
+ const DefaultTransports = Object.freeze(["webtransport", "websocket"]);
22
+ const DefaultFallbackDelayMs = 300;
23
+ const DefaultReprobeIntervalMs = 5 * 60_000;
24
+
25
+ /**
26
+ * A session that reconnects in the background after it is lost: WebTransport, or QMux over
27
+ * WebSocket when WebTransport is unavailable or blocked. Calls open their streams on the current
28
+ * session and fail fast with Unavailable while it reconnects; a call that was in flight when the
29
+ * session was lost is never replayed.
30
+ */
31
+ export class Channel {
32
+ #dial;
20
33
  #onStateChange;
21
- #options;
22
- #state = "connecting";
23
- #transport = null;
34
+ #session;
35
+ #transport;
36
+ #state = "ready";
24
37
  #waiters = [];
38
+ #stopped = new AbortController();
25
39
 
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));
40
+ /** Opens a channel and waits for its first session; use connect() instead. */
41
+ static async connect(url, options = {}) {
42
+ const dial = sessionDialer(url, options);
43
+ return new Channel(await dial(options.signal), dial, options.onStateChange);
34
44
  }
35
45
 
36
- /** Reports whether a generation is currently ready to accept new calls. */
37
- get ready() {
38
- return this.#state === "ready";
46
+ constructor(dialed, dial, onStateChange) {
47
+ this.#dial = dial;
48
+ this.#onStateChange = onStateChange;
49
+ this.#adopt(dialed);
39
50
  }
40
51
 
41
- /** Reports the channel lifecycle state. */
52
+ /** "ready", "reconnecting" while the session is being re-established, or "closed". */
42
53
  get state() {
43
54
  return this.#state;
44
55
  }
45
56
 
46
- /** Identifies the current successful connection generation, starting at one. */
47
- get generation() {
48
- return this.#generation;
57
+ /** The transport of the current or last session: "webtransport" or "websocket". */
58
+ get transport() {
59
+ return this.#transport;
49
60
  }
50
61
 
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"));
62
+ /** Opens a bidirectional stream on the current session. */
63
+ createBidirectionalStream() {
64
+ if (this.#state !== "ready") {
65
+ return Promise.reject(unavailable(`channel is ${this.#state}`));
58
66
  }
59
-
60
- return new Promise((resolve, reject) => {
61
- this.#waiters.push({ resolve, reject });
62
- });
67
+ return this.#session.createBidirectionalStream();
63
68
  }
64
69
 
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;
70
+ /**
71
+ * Opens a bidirectional stream on the current session, together with that session's WebTransport
72
+ * `datagrams` object, or undefined if the session has none (QMux over WebSocket). Calls that may
73
+ * use datagrams open their stream here, so their datagram flow belongs to the session that
74
+ * carries them even if the channel reconnects meanwhile.
75
+ */
76
+ createBidirectionalStreamWithDatagrams() {
77
+ if (this.#state !== "ready") {
78
+ return Promise.reject(unavailable(`channel is ${this.#state}`));
73
79
  }
80
+ const session = this.#session;
81
+ return session
82
+ .createBidirectionalStream()
83
+ .then((stream) => ({ stream, datagrams: session.datagrams }));
74
84
  }
75
85
 
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;
86
+ /** Resolves once the channel is ready, immediately if it already is. */
87
+ waitUntilReady() {
88
+ if (this.#state === "ready") {
89
+ return Promise.resolve();
85
90
  }
91
+ if (this.#state === "closed") {
92
+ return Promise.reject(unavailable("channel is closed"));
93
+ }
94
+ return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
86
95
  }
87
96
 
88
- /** Stops reconnecting and closes the current generation. */
89
- close(closeInfo = {}) {
97
+ /** Stops reconnecting and closes the current session. */
98
+ close(closeInfo) {
90
99
  if (this.#state === "closed") {
91
100
  return;
92
101
  }
93
-
94
- const transport = this.#transport;
95
- this.#transport = null;
96
- this.#connectAbort?.abort();
97
- this.#connectAbort = null;
102
+ const session = this.#session;
103
+ this.#session = undefined;
104
+ this.#stopped.abort();
98
105
  this.#transition("closed");
99
- this.#rejectWaiters(unavailable("channel is closed"));
100
- safeClose(transport, closeInfo);
106
+ for (const waiter of this.#waiters.splice(0)) {
107
+ waiter.reject(unavailable("channel is closed"));
108
+ }
109
+ closeQuietly(session, closeInfo);
101
110
  }
102
111
 
103
- #readyTransport() {
104
- if (!this.ready || this.#transport == null) {
105
- throw unavailable(`channel is ${this.#state}`);
106
- }
107
- return this.#transport;
112
+ #adopt({ session, transport }) {
113
+ this.#session = session;
114
+ this.#transport = transport;
115
+ const lost = (reason) => this.#lost(session, reason);
116
+ session.closed.then(lost, lost);
108
117
  }
109
118
 
110
- async #runConnectLoop(reconnecting, initialError) {
111
- if (this.#state === "closed" || this.#connectAbort != null) {
119
+ #lost(session, reason) {
120
+ if (session !== this.#session) {
112
121
  return;
113
122
  }
123
+ this.#session = undefined;
124
+ closeQuietly(session);
125
+ void this.#reconnect(reason);
126
+ }
114
127
 
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
- }
128
+ async #reconnect(error) {
129
+ const { signal } = this.#stopped;
130
+ for (let attempt = 1; !signal.aborted; attempt += 1) {
131
+ const delayMs = backoffDelay(attempt);
132
+ this.#transition("reconnecting", { attempt, delayMs, error });
133
+ await sleep(delayMs, signal);
134
+ if (signal.aborted) {
135
+ return;
137
136
  }
138
137
 
139
- let transport;
140
138
  try {
141
- transport = await this.#connect(controller.signal);
142
- } catch (connectError) {
143
- if (controller.signal.aborted) {
139
+ const dialed = await this.#dial(signal);
140
+ if (signal.aborted) {
141
+ closeQuietly(dialed.session);
144
142
  return;
145
143
  }
146
- error = connectError;
147
- continue;
148
- }
149
-
150
- if (controller.signal.aborted || this.#state === "closed") {
151
- safeClose(transport);
144
+ this.#adopt(dialed);
145
+ this.#transition("ready", { attempt, transport: dialed.transport });
146
+ for (const waiter of this.#waiters.splice(0)) {
147
+ waiter.resolve();
148
+ }
152
149
  return;
150
+ } catch (dialError) {
151
+ error = dialError;
153
152
  }
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
153
  }
191
154
  }
192
155
 
193
156
  #transition(state, extra = {}) {
194
157
  const previousState = this.#state;
195
158
  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
- }
159
+ try {
160
+ this.#onStateChange?.({ state, previousState, ...extra });
161
+ } catch (error) {
162
+ globalThis.reportError?.(error);
209
163
  }
210
- this.dispatchEvent(lifecycleEvent("statechange", detail));
211
- this.dispatchEvent(lifecycleEvent(state, detail));
212
164
  }
165
+ }
213
166
 
214
- #resolveWaiters() {
215
- for (const waiter of this.#waiters.splice(0)) {
216
- waiter.resolve();
167
+ /** Opens a channel to a TrevRPC server and waits for its session to be ready. */
168
+ export function connect(url, options = {}) {
169
+ return Channel.connect(url, options);
170
+ }
171
+
172
+ /**
173
+ * Returns `dial(signal)`, which establishes the next session as `{ session, transport }`.
174
+ *
175
+ * WebTransport is preferred. QMux over WebSocket starts in parallel once WebTransport fails or has
176
+ * not connected within `fallbackDelayMs`, and the first ready session wins. After WebSocket wins,
177
+ * reconnects use it directly until `reprobeIntervalMs` has passed.
178
+ */
179
+ function sessionDialer(url, options) {
180
+ const webSocketUrl = options.webSocketUrl ?? defaultWebSocketUrl(url);
181
+ const candidates = {
182
+ webtransport: {
183
+ transport: "webtransport",
184
+ dial: (signal) => dialWebTransport(url, options, signal),
185
+ },
186
+ websocket: {
187
+ transport: "websocket",
188
+ dial: (signal) => dialWebSocket(webSocketUrl, options, signal),
189
+ },
190
+ };
191
+ const [primary, fallback, ...rest] = (options.transports ?? DefaultTransports).map((name) => {
192
+ const candidate = candidates[name];
193
+ if (candidate === undefined) {
194
+ throw new TypeError(`unknown transport "${name}"`);
217
195
  }
196
+ return candidate;
197
+ });
198
+ if (primary === undefined || rest.length > 0 || primary === fallback) {
199
+ throw new TypeError("transports must list one or two distinct transports");
218
200
  }
201
+ const delayMs = options.fallbackDelayMs ?? DefaultFallbackDelayMs;
202
+ const reprobeIntervalMs = options.reprobeIntervalMs ?? DefaultReprobeIntervalMs;
203
+ let fallbackWonAt = null;
219
204
 
220
- #rejectWaiters(error) {
221
- for (const waiter of this.#waiters.splice(0)) {
222
- waiter.reject(error);
205
+ const dialOne = async (candidate, signal) => ({
206
+ session: await candidate.dial(signal),
207
+ transport: candidate.transport,
208
+ });
209
+ return async (signal) => {
210
+ if (fallback === undefined) {
211
+ return dialOne(primary, signal);
223
212
  }
224
- }
213
+ if (fallbackWonAt !== null && Date.now() - fallbackWonAt < reprobeIntervalMs) {
214
+ return dialOne(fallback, signal);
215
+ }
216
+ const dialed = await dialRace(primary, fallback, { delayMs, signal });
217
+ fallbackWonAt = dialed.transport === fallback.transport ? Date.now() : null;
218
+ return dialed;
219
+ };
225
220
  }
226
221
 
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)));
222
+ /** The `wss://` URL of the same endpoint as an `https://` URL. */
223
+ function defaultWebSocketUrl(url) {
224
+ const webSocketUrl = new URL(url);
225
+ webSocketUrl.protocol = webSocketUrl.protocol === "http:" ? "ws:" : "wss:";
226
+ return webSocketUrl.href;
230
227
  }
231
228
 
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");
229
+ /** Establishes a ready WebTransport session; aborting the signal abandons the attempt. */
230
+ function dialWebTransport(url, options, signal) {
231
+ const WebTransportConstructor = options.WebTransport ?? globalThis.WebTransport;
232
+ if (typeof WebTransportConstructor !== "function") {
233
+ throw unavailable("WebTransport is not available in this JavaScript runtime");
238
234
  }
239
-
240
- const signal = options.signal;
241
235
  if (signal?.aborted) {
242
- channel.close();
243
- throw initialAbortError(signal);
236
+ throw abortError(signal);
244
237
  }
245
- if (timeoutMs === 0) {
246
- channel.close();
247
- throw deadlineExceeded("initial connection deadline exceeded");
238
+ return establish(
239
+ new WebTransportConstructor(url, transportOptions(options)),
240
+ "WebTransport",
241
+ signal,
242
+ requireWireVersion,
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Fails a ready WebTransport session whose server selected another protocol than version 1. An
248
+ * empty or missing `protocol` means version 1: older browsers do not support the option, and the
249
+ * Rust server cannot echo a selection. A mismatch is FailedPrecondition rather than Unavailable
250
+ * because retrying the same server cannot fix it; the channel still treats it like any failed
251
+ * dial, so the WebSocket fallback and the reconnect loop carry on.
252
+ */
253
+ function requireWireVersion(session) {
254
+ const { protocol } = session;
255
+ if (typeof protocol === "string" && protocol !== "" && protocol !== WebTransportProtocol) {
256
+ throw failedPrecondition(
257
+ `server selected WebTransport protocol "${protocol}", but this client speaks "${WebTransportProtocol}"`,
258
+ );
248
259
  }
260
+ }
249
261
 
250
- const timeoutAbort = timeoutMs == null ? null : new AbortController();
251
- let removeAbort = () => {};
262
+ /** Establishes a ready QMux over WebSocket session; aborting the signal abandons the attempt. */
263
+ function dialWebSocket(url, options, signal) {
264
+ if (signal?.aborted) {
265
+ throw abortError(signal);
266
+ }
267
+ let session;
252
268
  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);
269
+ session = new QMuxWebSocketSession(url, { WebSocket: options.WebSocket });
275
270
  } catch (error) {
276
- channel.close();
277
- throw error;
278
- } finally {
279
- timeoutAbort?.abort();
280
- removeAbort();
271
+ throw statusFromTransportError(error);
281
272
  }
273
+ return establish(session, "WebSocket", signal);
282
274
  }
283
275
 
284
- function normalizeChannelOptions(options) {
285
- void options;
286
- return DefaultChannelOptions;
276
+ /**
277
+ * Waits for a new session to become ready, closing it if it fails or the signal aborts. `verify`
278
+ * may reject the ready session by throwing.
279
+ */
280
+ async function establish(session, name, signal, verify) {
281
+ const abandon = () => closeQuietly(session);
282
+ signal?.addEventListener("abort", abandon, { once: true });
283
+ try {
284
+ // A session that fails to connect settles both promises; one that closes first never becomes
285
+ // ready, so readiness must not wait for it.
286
+ await Promise.race([
287
+ session.ready,
288
+ session.closed.then(() => {
289
+ throw unavailable(`${name} session closed before becoming ready`);
290
+ }),
291
+ ]);
292
+ verify?.(session);
293
+ } catch (error) {
294
+ closeQuietly(session);
295
+ throw signal?.aborted ? abortError(signal) : statusFromTransportError(error);
296
+ } finally {
297
+ signal?.removeEventListener("abort", abandon);
298
+ }
299
+ if (signal?.aborted) {
300
+ closeQuietly(session);
301
+ throw abortError(signal);
302
+ }
303
+ return session;
287
304
  }
288
305
 
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 });
306
+ function transportOptions(options) {
307
+ const picked = { protocols: [WebTransportProtocol] };
308
+ for (const key of WebTransportOptionKeys) {
309
+ if (options[key] !== undefined) {
310
+ picked[key] = options[key];
311
+ }
296
312
  }
297
- return error;
313
+ return picked;
298
314
  }
299
315
 
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));
316
+ function abortError(signal) {
317
+ return cancelled("connection cancelled", signal.reason);
305
318
  }
306
319
 
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
- }
320
+ /** Exponential backoff with jitter, capped at maxDelayMs. */
321
+ function backoffDelay(attempt) {
322
+ const { initialDelayMs, maxDelayMs, jitter } = Backoff;
323
+ const base = Math.min(maxDelayMs, initialDelayMs * 2 ** (attempt - 1));
324
+ return Math.min(maxDelayMs, base * (1 + (Math.random() * 2 - 1) * jitter));
314
325
  }
315
326
 
316
- function waitForTimer(delayMs, signal) {
327
+ function sleep(delayMs, signal) {
317
328
  return new Promise((resolve) => {
318
- const timer = setTimeout(done, delayMs);
319
- signal.addEventListener("abort", done, { once: true });
320
-
321
- function done() {
329
+ const done = () => {
322
330
  clearTimeout(timer);
323
331
  signal.removeEventListener("abort", done);
324
332
  resolve();
325
- }
333
+ };
334
+ const timer = setTimeout(done, delayMs);
335
+ signal.addEventListener("abort", done, { once: true });
326
336
  });
327
337
  }
328
338
 
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) {
339
+ function closeQuietly(session, closeInfo) {
370
340
  try {
371
- transport?.close?.(closeInfo);
341
+ session?.close(closeInfo);
372
342
  } catch {
373
- // The failed generation may already have released its transport resources.
343
+ // The session may already be gone.
374
344
  }
375
345
  }