@replit/river 0.200.0-rc.2 → 0.200.0-rc.4

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 (84) hide show
  1. package/README.md +21 -20
  2. package/dist/chunk-2BF4VMUZ.js +50 -0
  3. package/dist/chunk-2BF4VMUZ.js.map +1 -0
  4. package/dist/chunk-BZQQXMVF.js +401 -0
  5. package/dist/chunk-BZQQXMVF.js.map +1 -0
  6. package/dist/{chunk-4VNY34QG.js → chunk-F7Z2QQRL.js} +24 -18
  7. package/dist/chunk-F7Z2QQRL.js.map +1 -0
  8. package/dist/chunk-IMFMNIEO.js +384 -0
  9. package/dist/chunk-IMFMNIEO.js.map +1 -0
  10. package/dist/chunk-RDGHFHXN.js +658 -0
  11. package/dist/chunk-RDGHFHXN.js.map +1 -0
  12. package/dist/chunk-SI4YHBTI.js +277 -0
  13. package/dist/chunk-SI4YHBTI.js.map +1 -0
  14. package/dist/{chunk-7CKIN3JT.js → chunk-SIRRYRLQ.js} +73 -490
  15. package/dist/chunk-SIRRYRLQ.js.map +1 -0
  16. package/dist/{chunk-S5RL45KH.js → chunk-V57VWV5S.js} +80 -44
  17. package/dist/chunk-V57VWV5S.js.map +1 -0
  18. package/dist/{chunk-QMM35C3H.js → chunk-VXYHC666.js} +1 -1
  19. package/dist/chunk-VXYHC666.js.map +1 -0
  20. package/dist/client-f56a6da3.d.ts +49 -0
  21. package/dist/{connection-f900e390.d.ts → connection-11991b13.d.ts} +1 -5
  22. package/dist/connection-6031a354.d.ts +11 -0
  23. package/dist/context-73df8978.d.ts +528 -0
  24. package/dist/logging/index.cjs.map +1 -1
  25. package/dist/logging/index.d.cts +1 -1
  26. package/dist/logging/index.d.ts +1 -1
  27. package/dist/logging/index.js +1 -1
  28. package/dist/{index-10ebd26a.d.ts → message-fd349b27.d.ts} +31 -31
  29. package/dist/router/index.cjs +125 -502
  30. package/dist/router/index.cjs.map +1 -1
  31. package/dist/router/index.d.cts +11 -46
  32. package/dist/router/index.d.ts +11 -46
  33. package/dist/router/index.js +2 -4
  34. package/dist/server-9f31d98f.d.ts +42 -0
  35. package/dist/{services-970f97bb.d.ts → services-c67758fc.d.ts} +5 -602
  36. package/dist/transport/impls/uds/client.cjs +1247 -1238
  37. package/dist/transport/impls/uds/client.cjs.map +1 -1
  38. package/dist/transport/impls/uds/client.d.cts +4 -3
  39. package/dist/transport/impls/uds/client.d.ts +4 -3
  40. package/dist/transport/impls/uds/client.js +7 -13
  41. package/dist/transport/impls/uds/client.js.map +1 -1
  42. package/dist/transport/impls/uds/server.cjs +1311 -1170
  43. package/dist/transport/impls/uds/server.cjs.map +1 -1
  44. package/dist/transport/impls/uds/server.d.cts +4 -4
  45. package/dist/transport/impls/uds/server.d.ts +4 -4
  46. package/dist/transport/impls/uds/server.js +6 -6
  47. package/dist/transport/impls/ws/client.cjs +988 -987
  48. package/dist/transport/impls/ws/client.cjs.map +1 -1
  49. package/dist/transport/impls/ws/client.d.cts +6 -5
  50. package/dist/transport/impls/ws/client.d.ts +6 -5
  51. package/dist/transport/impls/ws/client.js +6 -7
  52. package/dist/transport/impls/ws/client.js.map +1 -1
  53. package/dist/transport/impls/ws/server.cjs +1192 -1066
  54. package/dist/transport/impls/ws/server.cjs.map +1 -1
  55. package/dist/transport/impls/ws/server.d.cts +4 -4
  56. package/dist/transport/impls/ws/server.d.ts +4 -4
  57. package/dist/transport/impls/ws/server.js +6 -6
  58. package/dist/transport/index.cjs +1446 -1380
  59. package/dist/transport/index.cjs.map +1 -1
  60. package/dist/transport/index.d.cts +4 -26
  61. package/dist/transport/index.d.ts +4 -26
  62. package/dist/transport/index.js +9 -9
  63. package/dist/util/testHelpers.cjs +746 -303
  64. package/dist/util/testHelpers.cjs.map +1 -1
  65. package/dist/util/testHelpers.d.cts +9 -4
  66. package/dist/util/testHelpers.d.ts +9 -4
  67. package/dist/util/testHelpers.js +36 -10
  68. package/dist/util/testHelpers.js.map +1 -1
  69. package/package.json +1 -1
  70. package/dist/chunk-47TFNAY2.js +0 -476
  71. package/dist/chunk-47TFNAY2.js.map +0 -1
  72. package/dist/chunk-4VNY34QG.js.map +0 -1
  73. package/dist/chunk-7CKIN3JT.js.map +0 -1
  74. package/dist/chunk-CZP4LK3F.js +0 -335
  75. package/dist/chunk-CZP4LK3F.js.map +0 -1
  76. package/dist/chunk-DJCW3SKT.js +0 -59
  77. package/dist/chunk-DJCW3SKT.js.map +0 -1
  78. package/dist/chunk-NQWDT6GS.js +0 -347
  79. package/dist/chunk-NQWDT6GS.js.map +0 -1
  80. package/dist/chunk-ONUXWVRC.js +0 -492
  81. package/dist/chunk-ONUXWVRC.js.map +0 -1
  82. package/dist/chunk-QMM35C3H.js.map +0 -1
  83. package/dist/chunk-S5RL45KH.js.map +0 -1
  84. package/dist/connection-3f117047.d.ts +0 -17
@@ -25,12 +25,193 @@ __export(client_exports, {
25
25
  module.exports = __toCommonJS(client_exports);
26
26
  var import_node_net = require("net");
27
27
 
28
- // transport/session.ts
29
- var import_nanoid2 = require("nanoid");
28
+ // transport/transforms/messageFraming.ts
29
+ var import_node_stream = require("stream");
30
+ var Uint32LengthPrefixFraming = class extends import_node_stream.Transform {
31
+ receivedBuffer;
32
+ maxBufferSizeBytes;
33
+ constructor({ maxBufferSizeBytes, ...options }) {
34
+ super(options);
35
+ this.maxBufferSizeBytes = maxBufferSizeBytes;
36
+ this.receivedBuffer = Buffer.alloc(0);
37
+ }
38
+ _transform(chunk, _encoding, cb) {
39
+ if (this.receivedBuffer.byteLength + chunk.byteLength > this.maxBufferSizeBytes) {
40
+ const err = new Error(
41
+ `buffer overflow: ${this.receivedBuffer.byteLength}B > ${this.maxBufferSizeBytes}B`
42
+ );
43
+ this.emit("error", err);
44
+ cb(err);
45
+ return;
46
+ }
47
+ this.receivedBuffer = Buffer.concat([this.receivedBuffer, chunk]);
48
+ while (this.receivedBuffer.length > 4) {
49
+ const claimedMessageLength = this.receivedBuffer.readUInt32BE(0) + 4;
50
+ if (this.receivedBuffer.length >= claimedMessageLength) {
51
+ const message = this.receivedBuffer.subarray(4, claimedMessageLength);
52
+ this.push(message);
53
+ this.receivedBuffer = this.receivedBuffer.subarray(claimedMessageLength);
54
+ } else {
55
+ break;
56
+ }
57
+ }
58
+ cb();
59
+ }
60
+ _flush(cb) {
61
+ if (this.receivedBuffer.length) {
62
+ this.emit("error", new Error("got incomplete message while flushing"));
63
+ }
64
+ this.receivedBuffer = Buffer.alloc(0);
65
+ cb();
66
+ }
67
+ _destroy(error, callback) {
68
+ this.receivedBuffer = Buffer.alloc(0);
69
+ super._destroy(error, callback);
70
+ }
71
+ };
72
+ function createLengthEncodedStream(options) {
73
+ return new Uint32LengthPrefixFraming({
74
+ maxBufferSizeBytes: options?.maxBufferSizeBytes ?? 16 * 1024 * 1024
75
+ // 16MB
76
+ });
77
+ }
78
+ var MessageFramer = {
79
+ createFramedStream: createLengthEncodedStream,
80
+ write: (buf) => {
81
+ const lengthPrefix = Buffer.alloc(4);
82
+ lengthPrefix.writeUInt32BE(buf.length, 0);
83
+ return Buffer.concat([lengthPrefix, buf]);
84
+ }
85
+ };
86
+
87
+ // transport/id.ts
88
+ var import_nanoid = require("nanoid");
89
+ var alphabet = (0, import_nanoid.customAlphabet)(
90
+ "1234567890abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ"
91
+ );
92
+ var generateId = () => alphabet(12);
93
+
94
+ // transport/connection.ts
95
+ var Connection = class {
96
+ id;
97
+ telemetry;
98
+ constructor() {
99
+ this.id = `conn-${generateId()}`;
100
+ }
101
+ get loggingMetadata() {
102
+ const metadata = { connId: this.id };
103
+ const spanContext = this.telemetry?.span.spanContext();
104
+ if (this.telemetry?.span.isRecording() && spanContext) {
105
+ metadata.telemetry = {
106
+ traceId: spanContext.traceId,
107
+ spanId: spanContext.spanId
108
+ };
109
+ }
110
+ return metadata;
111
+ }
112
+ // can't use event emitter because we need this to work in both node + browser
113
+ _dataListeners = /* @__PURE__ */ new Set();
114
+ _closeListeners = /* @__PURE__ */ new Set();
115
+ _errorListeners = /* @__PURE__ */ new Set();
116
+ get dataListeners() {
117
+ return [...this._dataListeners];
118
+ }
119
+ get closeListeners() {
120
+ return [...this._closeListeners];
121
+ }
122
+ get errorListeners() {
123
+ return [...this._errorListeners];
124
+ }
125
+ /**
126
+ * Handle adding a callback for when a message is received.
127
+ * @param msg The message that was received.
128
+ */
129
+ addDataListener(cb) {
130
+ this._dataListeners.add(cb);
131
+ }
132
+ removeDataListener(cb) {
133
+ this._dataListeners.delete(cb);
134
+ }
135
+ /**
136
+ * Handle adding a callback for when the connection is closed.
137
+ * This should also be called if an error happens and after notifying all the error listeners.
138
+ * @param cb The callback to call when the connection is closed.
139
+ */
140
+ addCloseListener(cb) {
141
+ this._closeListeners.add(cb);
142
+ }
143
+ removeCloseListener(cb) {
144
+ this._closeListeners.delete(cb);
145
+ }
146
+ /**
147
+ * Handle adding a callback for when an error is received.
148
+ * This should only be used for this.logging errors, all cleanup
149
+ * should be delegated to addCloseListener.
150
+ *
151
+ * The implementer should take care such that the implemented
152
+ * connection will call both the close and error callbacks
153
+ * on an error.
154
+ *
155
+ * @param cb The callback to call when an error is received.
156
+ */
157
+ addErrorListener(cb) {
158
+ this._errorListeners.add(cb);
159
+ }
160
+ removeErrorListener(cb) {
161
+ this._errorListeners.delete(cb);
162
+ }
163
+ };
164
+
165
+ // transport/impls/uds/connection.ts
166
+ var UdsConnection = class extends Connection {
167
+ sock;
168
+ input;
169
+ framer;
170
+ constructor(sock) {
171
+ super();
172
+ this.framer = MessageFramer.createFramedStream();
173
+ this.sock = sock;
174
+ this.input = sock.pipe(this.framer);
175
+ this.sock.on("close", () => {
176
+ for (const cb of this.closeListeners) {
177
+ cb();
178
+ }
179
+ });
180
+ this.sock.on("error", (err) => {
181
+ if (err instanceof Error && "code" in err && err.code === "EPIPE") {
182
+ return;
183
+ }
184
+ for (const cb of this.errorListeners) {
185
+ cb(err);
186
+ }
187
+ });
188
+ this.input.on("data", (msg) => {
189
+ for (const cb of this.dataListeners) {
190
+ cb(msg);
191
+ }
192
+ });
193
+ this.sock.on("end", () => {
194
+ this.sock.destroy();
195
+ });
196
+ }
197
+ send(payload) {
198
+ if (this.framer.destroyed || !this.sock.writable || this.sock.closed) {
199
+ return false;
200
+ }
201
+ this.sock.write(MessageFramer.write(payload));
202
+ return true;
203
+ }
204
+ close() {
205
+ this.sock.end();
206
+ this.framer.end();
207
+ }
208
+ };
209
+
210
+ // transport/client.ts
211
+ var import_api3 = require("@opentelemetry/api");
30
212
 
31
213
  // transport/message.ts
32
214
  var import_typebox = require("@sinclair/typebox");
33
- var import_nanoid = require("nanoid");
34
215
  var TransportMessageSchema = (t) => import_typebox.Type.Object({
35
216
  id: import_typebox.Type.String(),
36
217
  from: import_typebox.Type.String(),
@@ -55,7 +236,7 @@ var ControlMessageAckSchema = import_typebox.Type.Object({
55
236
  var ControlMessageCloseSchema = import_typebox.Type.Object({
56
237
  type: import_typebox.Type.Literal("CLOSE")
57
238
  });
58
- var PROTOCOL_VERSION = "v2.0";
239
+ var currentProtocolVersion = "v2.0";
59
240
  var ControlMessageHandshakeRequestSchema = import_typebox.Type.Object({
60
241
  type: import_typebox.Type.Literal("HANDSHAKE_REQ"),
61
242
  protocolVersion: import_typebox.Type.String(),
@@ -65,18 +246,29 @@ var ControlMessageHandshakeRequestSchema = import_typebox.Type.Object({
65
246
  * used by the server to know whether this is a new or a reestablished connection, and whether it
66
247
  * is compatible with what it already has.
67
248
  */
68
- expectedSessionState: import_typebox.Type.Optional(
69
- import_typebox.Type.Object({
70
- /**
71
- * reconnect is set to true if the client explicitly wants to reestablish an existing
72
- * connection.
73
- */
74
- reconnect: import_typebox.Type.Boolean(),
75
- nextExpectedSeq: import_typebox.Type.Integer()
76
- })
77
- ),
249
+ expectedSessionState: import_typebox.Type.Object({
250
+ // what the client expects the server to send next
251
+ nextExpectedSeq: import_typebox.Type.Integer(),
252
+ // TODO: remove optional once we know all servers
253
+ // are nextSentSeq here
254
+ // what the server expects the client to send next
255
+ nextSentSeq: import_typebox.Type.Optional(import_typebox.Type.Integer())
256
+ }),
78
257
  metadata: import_typebox.Type.Optional(import_typebox.Type.Unknown())
79
258
  });
259
+ var HandshakeErrorRetriableResponseCodes = import_typebox.Type.Union([
260
+ import_typebox.Type.Literal("SESSION_STATE_MISMATCH")
261
+ ]);
262
+ var HandshakeErrorFatalResponseCodes = import_typebox.Type.Union([
263
+ import_typebox.Type.Literal("MALFORMED_HANDSHAKE_META"),
264
+ import_typebox.Type.Literal("MALFORMED_HANDSHAKE"),
265
+ import_typebox.Type.Literal("PROTOCOL_VERSION_MISMATCH"),
266
+ import_typebox.Type.Literal("REJECTED_BY_CUSTOM_HANDLER")
267
+ ]);
268
+ var HandshakeErrorResponseCodes = import_typebox.Type.Union([
269
+ HandshakeErrorRetriableResponseCodes,
270
+ HandshakeErrorFatalResponseCodes
271
+ ]);
80
272
  var ControlMessageHandshakeResponseSchema = import_typebox.Type.Object({
81
273
  type: import_typebox.Type.Literal("HANDSHAKE_RESP"),
82
274
  status: import_typebox.Type.Union([
@@ -86,7 +278,10 @@ var ControlMessageHandshakeResponseSchema = import_typebox.Type.Object({
86
278
  }),
87
279
  import_typebox.Type.Object({
88
280
  ok: import_typebox.Type.Literal(false),
89
- reason: import_typebox.Type.String()
281
+ reason: import_typebox.Type.String(),
282
+ // TODO: remove optional once we know all servers
283
+ // are sending code here
284
+ code: import_typebox.Type.Optional(HandshakeErrorResponseCodes)
90
285
  })
91
286
  ])
92
287
  });
@@ -108,536 +303,75 @@ function handshakeRequestMessage({
108
303
  tracing
109
304
  }) {
110
305
  return {
111
- id: (0, import_nanoid.nanoid)(),
306
+ id: generateId(),
112
307
  from,
113
308
  to,
114
309
  seq: 0,
115
310
  ack: 0,
116
- streamId: (0, import_nanoid.nanoid)(),
311
+ streamId: generateId(),
117
312
  controlFlags: 0,
118
313
  tracing,
119
314
  payload: {
120
315
  type: "HANDSHAKE_REQ",
121
- protocolVersion: PROTOCOL_VERSION,
316
+ protocolVersion: currentProtocolVersion,
122
317
  sessionId,
123
318
  expectedSessionState,
124
319
  metadata
125
320
  }
126
321
  };
127
322
  }
128
- var SESSION_STATE_MISMATCH = "session state mismatch";
129
323
  function isAck(controlFlag) {
130
324
  return (controlFlag & 1 /* AckBit */) === 1 /* AckBit */;
131
325
  }
132
326
 
133
- // tracing/index.ts
134
- var import_api = require("@opentelemetry/api");
135
-
136
- // package.json
137
- var version = "0.200.0-rc.2";
138
-
139
- // tracing/index.ts
140
- function getPropagationContext(ctx) {
141
- const tracing = {
142
- traceparent: "",
143
- tracestate: ""
144
- };
145
- import_api.propagation.inject(ctx, tracing);
146
- return tracing;
147
- }
148
- function createSessionTelemetryInfo(session, propagationCtx) {
149
- const parentCtx = propagationCtx ? import_api.propagation.extract(import_api.context.active(), propagationCtx) : import_api.context.active();
150
- const span = tracer.startSpan(
151
- `session ${session.id}`,
152
- {
153
- attributes: {
154
- component: "river",
155
- "river.session.id": session.id,
156
- "river.session.to": session.to,
157
- "river.session.from": session.from
158
- }
159
- },
160
- parentCtx
161
- );
162
- const ctx = import_api.trace.setSpan(parentCtx, span);
163
- return { span, ctx };
164
- }
165
- function createConnectionTelemetryInfo(connection, info) {
166
- const span = tracer.startSpan(
167
- `connection ${connection.id}`,
168
- {
169
- attributes: {
170
- component: "river",
171
- "river.connection.id": connection.id
172
- },
173
- links: [{ context: info.span.spanContext() }]
174
- },
175
- info.ctx
176
- );
177
- const ctx = import_api.trace.setSpan(info.ctx, span);
178
- return { span, ctx };
327
+ // codec/json.ts
328
+ var encoder = new TextEncoder();
329
+ var decoder = new TextDecoder();
330
+ function uint8ArrayToBase64(uint8Array) {
331
+ let binary = "";
332
+ uint8Array.forEach((byte) => {
333
+ binary += String.fromCharCode(byte);
334
+ });
335
+ return btoa(binary);
179
336
  }
180
- var tracer = import_api.trace.getTracer("river", version);
181
- var tracing_default = tracer;
182
-
183
- // transport/session.ts
184
- var import_api2 = require("@opentelemetry/api");
185
- var nanoid2 = (0, import_nanoid2.customAlphabet)("1234567890abcdefghijklmnopqrstuvxyz", 6);
186
- var unsafeId = () => nanoid2();
187
- var Connection = class {
188
- id;
189
- telemetry;
190
- constructor() {
191
- this.id = `conn-${nanoid2(12)}`;
337
+ function base64ToUint8Array(base64) {
338
+ const binaryString = atob(base64);
339
+ const uint8Array = new Uint8Array(binaryString.length);
340
+ for (let i = 0; i < binaryString.length; i++) {
341
+ uint8Array[i] = binaryString.charCodeAt(i);
192
342
  }
193
- get loggingMetadata() {
194
- const metadata = { connId: this.id };
195
- const spanContext = this.telemetry?.span.spanContext();
196
- if (this.telemetry?.span.isRecording() && spanContext) {
197
- metadata.telemetry = {
198
- traceId: spanContext.traceId,
199
- spanId: spanContext.spanId
200
- };
201
- }
202
- return metadata;
203
- }
204
- };
205
- var Session = class {
206
- codec;
207
- options;
208
- telemetry;
209
- /**
210
- * The buffer of messages that have been sent but not yet acknowledged.
211
- */
212
- sendBuffer = [];
213
- /**
214
- * The active connection associated with this session
215
- */
216
- connection;
217
- /**
218
- * A connection that is currently undergoing handshaking. Used to distinguish between the active
219
- * connection, but still be able to close it if needed.
220
- */
221
- handshakingConnection;
222
- from;
223
- to;
224
- /**
225
- * The unique ID of this session.
226
- */
227
- id;
228
- /**
229
- * What the other side advertised as their session ID
230
- * for this session.
231
- */
232
- advertisedSessionId;
233
- /**
234
- * Number of messages we've sent along this session (excluding handshake and acks)
235
- */
236
- seq = 0;
237
- /**
238
- * Number of unique messages we've received this session (excluding handshake and acks)
239
- */
240
- ack = 0;
241
- /**
242
- * The grace period between when the inner connection is disconnected
243
- * and when we should consider the entire session disconnected.
244
- */
245
- disconnectionGrace;
246
- /**
247
- * Number of heartbeats we've sent without a response.
248
- */
249
- heartbeatMisses;
250
- /**
251
- * The interval for sending heartbeats.
252
- */
253
- heartbeat;
254
- log;
255
- constructor(conn, from, to, options, propagationCtx) {
256
- this.id = `session-${nanoid2(12)}`;
257
- this.options = options;
258
- this.from = from;
259
- this.to = to;
260
- this.connection = conn;
261
- this.codec = options.codec;
262
- this.heartbeatMisses = 0;
263
- this.heartbeat = setInterval(
264
- () => this.sendHeartbeat(),
265
- options.heartbeatIntervalMs
266
- );
267
- this.telemetry = createSessionTelemetryInfo(this, propagationCtx);
268
- }
269
- bindLogger(log) {
270
- this.log = log;
271
- }
272
- get loggingMetadata() {
273
- const spanContext = this.telemetry.span.spanContext();
274
- return {
275
- clientId: this.from,
276
- connectedTo: this.to,
277
- sessionId: this.id,
278
- connId: this.connection?.id,
279
- telemetry: {
280
- traceId: spanContext.traceId,
281
- spanId: spanContext.spanId
282
- }
283
- };
284
- }
285
- /**
286
- * Sends a message over the session's connection.
287
- * If the connection is not ready or the message fails to send, the message can be buffered for retry unless skipped.
288
- *
289
- * @param msg The partial message to be sent, which will be constructed into a full message.
290
- * @param addToSendBuff Whether to add the message to the send buffer for retry.
291
- * @returns The full transport ID of the message that was attempted to be sent.
292
- */
293
- send(msg) {
294
- const fullMsg = this.constructMsg(msg);
295
- this.log?.debug(`sending msg`, {
296
- ...this.loggingMetadata,
297
- transportMessage: fullMsg
298
- });
299
- if (this.connection) {
300
- const ok = this.connection.send(this.codec.toBuffer(fullMsg));
301
- if (ok)
302
- return fullMsg.id;
303
- this.log?.info(
304
- `failed to send msg to ${fullMsg.to}, connection is probably dead`,
305
- {
306
- ...this.loggingMetadata,
307
- transportMessage: fullMsg
308
- }
309
- );
310
- } else {
311
- this.log?.debug(
312
- `buffering msg to ${fullMsg.to}, connection not ready yet`,
313
- { ...this.loggingMetadata, transportMessage: fullMsg }
314
- );
315
- }
316
- return fullMsg.id;
317
- }
318
- sendHeartbeat() {
319
- const misses = this.heartbeatMisses;
320
- const missDuration = misses * this.options.heartbeatIntervalMs;
321
- if (misses > this.options.heartbeatsUntilDead) {
322
- if (this.connection) {
323
- this.log?.info(
324
- `closing connection to ${this.to} due to inactivity (missed ${misses} heartbeats which is ${missDuration}ms)`,
325
- this.loggingMetadata
326
- );
327
- this.telemetry.span.addEvent("closing connection due to inactivity");
328
- this.closeStaleConnection();
329
- }
330
- return;
331
- }
332
- this.send({
333
- streamId: "heartbeat",
334
- controlFlags: 1 /* AckBit */,
335
- payload: {
336
- type: "ACK"
337
- }
338
- });
339
- this.heartbeatMisses++;
340
- }
341
- resetBufferedMessages() {
342
- this.sendBuffer = [];
343
- this.seq = 0;
344
- this.ack = 0;
345
- }
346
- sendBufferedMessages(conn) {
347
- this.log?.info(`resending ${this.sendBuffer.length} buffered messages`, {
348
- ...this.loggingMetadata,
349
- connId: conn.id
350
- });
351
- for (const msg of this.sendBuffer) {
352
- this.log?.debug(`resending msg`, {
353
- ...this.loggingMetadata,
354
- transportMessage: msg,
355
- connId: conn.id
356
- });
357
- const ok = conn.send(this.codec.toBuffer(msg));
358
- if (!ok) {
359
- const errMsg = `failed to send buffered message to ${this.to} (sus, this is a fresh connection)`;
360
- conn.telemetry?.span.setStatus({
361
- code: import_api2.SpanStatusCode.ERROR,
362
- message: errMsg
363
- });
364
- this.log?.error(errMsg, {
365
- ...this.loggingMetadata,
366
- transportMessage: msg,
367
- connId: conn.id,
368
- tags: ["invariant-violation"]
369
- });
370
- conn.close();
371
- return;
372
- }
373
- }
374
- }
375
- updateBookkeeping(ack, seq) {
376
- if (seq + 1 < this.ack) {
377
- this.log?.error(`received stale seq ${seq} + 1 < ${this.ack}`, {
378
- ...this.loggingMetadata,
379
- tags: ["invariant-violation"]
380
- });
381
- return;
382
- }
383
- this.sendBuffer = this.sendBuffer.filter((unacked) => unacked.seq >= ack);
384
- this.ack = seq + 1;
385
- }
386
- closeStaleConnection(conn) {
387
- if (this.connection === void 0 || this.connection === conn)
388
- return;
389
- this.log?.info(
390
- `closing old inner connection from session to ${this.to}`,
391
- this.loggingMetadata
392
- );
393
- this.connection.close();
394
- this.connection = void 0;
395
- }
396
- replaceWithNewConnection(newConn, isTransparentReconnect) {
397
- this.closeStaleConnection(newConn);
398
- this.cancelGrace();
399
- if (isTransparentReconnect) {
400
- this.sendBufferedMessages(newConn);
401
- }
402
- this.connection = newConn;
403
- this.handshakingConnection = void 0;
404
- }
405
- replaceWithNewHandshakingConnection(newConn) {
406
- this.handshakingConnection = newConn;
407
- }
408
- beginGrace(cb) {
409
- this.log?.info(
410
- `starting ${this.options.sessionDisconnectGraceMs}ms grace period until session to ${this.to} is closed`,
411
- this.loggingMetadata
412
- );
413
- this.cancelGrace();
414
- this.disconnectionGrace = setTimeout(() => {
415
- this.log?.info(
416
- `grace period for ${this.to} elapsed`,
417
- this.loggingMetadata
418
- );
419
- cb();
420
- }, this.options.sessionDisconnectGraceMs);
421
- }
422
- // called on reconnect of the underlying session
423
- cancelGrace() {
424
- this.heartbeatMisses = 0;
425
- clearTimeout(this.disconnectionGrace);
426
- this.disconnectionGrace = void 0;
427
- }
428
- /**
429
- * Used to close the handshaking connection, if set.
430
- */
431
- closeHandshakingConnection(expectedHandshakingConn) {
432
- if (this.handshakingConnection === void 0)
433
- return;
434
- if (expectedHandshakingConn !== void 0 && this.handshakingConnection === expectedHandshakingConn) {
435
- return;
436
- }
437
- this.handshakingConnection.close();
438
- this.handshakingConnection = void 0;
439
- }
440
- // closed when we want to discard the whole session
441
- // (i.e. shutdown or session disconnect)
442
- close() {
443
- this.closeStaleConnection();
444
- this.cancelGrace();
445
- this.resetBufferedMessages();
446
- clearInterval(this.heartbeat);
447
- }
448
- get connected() {
449
- return this.connection !== void 0;
450
- }
451
- get nextExpectedAck() {
452
- return this.seq;
453
- }
454
- get nextExpectedSeq() {
455
- return this.ack;
456
- }
457
- /**
458
- * Check that the peer's next expected seq number matches something that is in our send buffer
459
- * _or_ matches our actual next seq.
460
- */
461
- nextExpectedSeqInRange(nextExpectedSeq) {
462
- for (const msg of this.sendBuffer) {
463
- if (nextExpectedSeq === msg.seq) {
464
- return true;
465
- }
466
- }
467
- return nextExpectedSeq === this.seq;
468
- }
469
- // This is only used in tests to make the session misbehave.
470
- /* @internal */
471
- advanceAckForTesting(by) {
472
- this.ack += by;
473
- }
474
- constructMsg(partialMsg) {
475
- const msg = {
476
- ...partialMsg,
477
- id: unsafeId(),
478
- to: this.to,
479
- from: this.from,
480
- seq: this.seq,
481
- ack: this.ack
482
- };
483
- this.seq++;
484
- this.sendBuffer.push(msg);
485
- return msg;
486
- }
487
- inspectSendBuffer() {
488
- return this.sendBuffer;
489
- }
490
- };
491
-
492
- // transport/transforms/messageFraming.ts
493
- var import_node_stream = require("stream");
494
- var Uint32LengthPrefixFraming = class extends import_node_stream.Transform {
495
- receivedBuffer;
496
- maxBufferSizeBytes;
497
- constructor({ maxBufferSizeBytes, ...options }) {
498
- super(options);
499
- this.maxBufferSizeBytes = maxBufferSizeBytes;
500
- this.receivedBuffer = Buffer.alloc(0);
501
- }
502
- _transform(chunk, _encoding, cb) {
503
- if (this.receivedBuffer.byteLength + chunk.byteLength > this.maxBufferSizeBytes) {
504
- const err = new Error(
505
- `buffer overflow: ${this.receivedBuffer.byteLength}B > ${this.maxBufferSizeBytes}B`
506
- );
507
- this.emit("error", err);
508
- cb(err);
509
- return;
510
- }
511
- this.receivedBuffer = Buffer.concat([this.receivedBuffer, chunk]);
512
- while (this.receivedBuffer.length > 4) {
513
- const claimedMessageLength = this.receivedBuffer.readUInt32BE(0) + 4;
514
- if (this.receivedBuffer.length >= claimedMessageLength) {
515
- const message = this.receivedBuffer.subarray(4, claimedMessageLength);
516
- this.push(message);
517
- this.receivedBuffer = this.receivedBuffer.subarray(claimedMessageLength);
518
- } else {
519
- break;
520
- }
521
- }
522
- cb();
523
- }
524
- _flush(cb) {
525
- if (this.receivedBuffer.length) {
526
- this.emit("error", new Error("got incomplete message while flushing"));
527
- }
528
- this.receivedBuffer = Buffer.alloc(0);
529
- cb();
530
- }
531
- _destroy(error, callback) {
532
- this.receivedBuffer = Buffer.alloc(0);
533
- super._destroy(error, callback);
534
- }
535
- };
536
- function createLengthEncodedStream(options) {
537
- return new Uint32LengthPrefixFraming({
538
- maxBufferSizeBytes: options?.maxBufferSizeBytes ?? 16 * 1024 * 1024
539
- // 16MB
540
- });
541
- }
542
- var MessageFramer = {
543
- createFramedStream: createLengthEncodedStream,
544
- write: (buf) => {
545
- const lengthPrefix = Buffer.alloc(4);
546
- lengthPrefix.writeUInt32BE(buf.length, 0);
547
- return Buffer.concat([lengthPrefix, buf]);
548
- }
549
- };
550
-
551
- // transport/impls/uds/connection.ts
552
- var UdsConnection = class extends Connection {
553
- sock;
554
- input;
555
- framer;
556
- constructor(sock) {
557
- super();
558
- this.framer = MessageFramer.createFramedStream();
559
- this.sock = sock;
560
- this.input = sock.pipe(this.framer);
561
- }
562
- addDataListener(cb) {
563
- this.input.on("data", cb);
564
- }
565
- removeDataListener(cb) {
566
- this.input.off("data", cb);
567
- }
568
- addCloseListener(cb) {
569
- this.sock.on("close", cb);
570
- }
571
- addErrorListener(cb) {
572
- this.sock.on("error", (err) => {
573
- if (err instanceof Error && "code" in err && err.code === "EPIPE") {
574
- return;
575
- }
576
- cb(err);
577
- });
578
- }
579
- send(payload) {
580
- if (this.framer.destroyed || !this.sock.writable)
581
- return false;
582
- return this.sock.write(MessageFramer.write(payload));
583
- }
584
- close() {
585
- this.sock.destroy();
586
- this.framer.destroy();
587
- }
588
- };
589
-
590
- // transport/client.ts
591
- var import_api4 = require("@opentelemetry/api");
592
-
593
- // codec/json.ts
594
- var encoder = new TextEncoder();
595
- var decoder = new TextDecoder();
596
- function uint8ArrayToBase64(uint8Array) {
597
- let binary = "";
598
- uint8Array.forEach((byte) => {
599
- binary += String.fromCharCode(byte);
600
- });
601
- return btoa(binary);
602
- }
603
- function base64ToUint8Array(base64) {
604
- const binaryString = atob(base64);
605
- const uint8Array = new Uint8Array(binaryString.length);
606
- for (let i = 0; i < binaryString.length; i++) {
607
- uint8Array[i] = binaryString.charCodeAt(i);
608
- }
609
- return uint8Array;
610
- }
611
- var NaiveJsonCodec = {
612
- toBuffer: (obj) => {
613
- return encoder.encode(
614
- JSON.stringify(obj, function replacer(key) {
615
- const val = this[key];
616
- if (val instanceof Uint8Array) {
617
- return { $t: uint8ArrayToBase64(val) };
618
- } else {
619
- return val;
620
- }
621
- })
622
- );
623
- },
624
- fromBuffer: (buff) => {
625
- try {
626
- const parsed = JSON.parse(
627
- decoder.decode(buff),
628
- function reviver(_key, val) {
629
- if (val?.$t) {
630
- return base64ToUint8Array(val.$t);
631
- } else {
632
- return val;
633
- }
634
- }
635
- );
636
- if (typeof parsed === "object")
637
- return parsed;
638
- return null;
639
- } catch {
640
- return null;
343
+ return uint8Array;
344
+ }
345
+ var NaiveJsonCodec = {
346
+ toBuffer: (obj) => {
347
+ return encoder.encode(
348
+ JSON.stringify(obj, function replacer(key) {
349
+ const val = this[key];
350
+ if (val instanceof Uint8Array) {
351
+ return { $t: uint8ArrayToBase64(val) };
352
+ } else {
353
+ return val;
354
+ }
355
+ })
356
+ );
357
+ },
358
+ fromBuffer: (buff) => {
359
+ try {
360
+ const parsed = JSON.parse(
361
+ decoder.decode(buff),
362
+ function reviver(_key, val) {
363
+ if (val?.$t) {
364
+ return base64ToUint8Array(val.$t);
365
+ } else {
366
+ return val;
367
+ }
368
+ }
369
+ );
370
+ if (typeof parsed === "object")
371
+ return parsed;
372
+ return null;
373
+ } catch {
374
+ return null;
641
375
  }
642
376
  }
643
377
  };
@@ -647,6 +381,8 @@ var defaultTransportOptions = {
647
381
  heartbeatIntervalMs: 1e3,
648
382
  heartbeatsUntilDead: 2,
649
383
  sessionDisconnectGraceMs: 5e3,
384
+ connectionTimeoutMs: 2e3,
385
+ handshakeTimeoutMs: 1e3,
650
386
  codec: NaiveJsonCodec
651
387
  };
652
388
  var defaultConnectionRetryOptions = {
@@ -735,9 +471,6 @@ var LeakyBucketRateLimit = class {
735
471
  }
736
472
  };
737
473
 
738
- // transport/transport.ts
739
- var import_value = require("@sinclair/typebox/value");
740
-
741
474
  // logging/log.ts
742
475
  var LoggingLevels = {
743
476
  debug: -1,
@@ -773,411 +506,752 @@ var BaseLogger = class {
773
506
  this.output(msg, metadata ?? {}, "info");
774
507
  }
775
508
  }
776
- warn(msg, metadata) {
777
- if (LoggingLevels[this.minLevel] <= LoggingLevels.warn) {
778
- this.output(msg, metadata ?? {}, "warn");
779
- }
509
+ warn(msg, metadata) {
510
+ if (LoggingLevels[this.minLevel] <= LoggingLevels.warn) {
511
+ this.output(msg, metadata ?? {}, "warn");
512
+ }
513
+ }
514
+ error(msg, metadata) {
515
+ if (LoggingLevels[this.minLevel] <= LoggingLevels.error) {
516
+ this.output(msg, metadata ?? {}, "error");
517
+ }
518
+ }
519
+ };
520
+ var createLogProxy = (log) => ({
521
+ debug: cleanedLogFn(log.debug.bind(log)),
522
+ info: cleanedLogFn(log.info.bind(log)),
523
+ warn: cleanedLogFn(log.warn.bind(log)),
524
+ error: cleanedLogFn(log.error.bind(log))
525
+ });
526
+
527
+ // transport/events.ts
528
+ var ProtocolError = {
529
+ RetriesExceeded: "conn_retry_exceeded",
530
+ HandshakeFailed: "handshake_failed",
531
+ MessageOrderingViolated: "message_ordering_violated"
532
+ };
533
+ var EventDispatcher = class {
534
+ eventListeners = {};
535
+ removeAllListeners() {
536
+ this.eventListeners = {};
537
+ }
538
+ numberOfListeners(eventType) {
539
+ return this.eventListeners[eventType]?.size ?? 0;
540
+ }
541
+ addEventListener(eventType, handler) {
542
+ if (!this.eventListeners[eventType]) {
543
+ this.eventListeners[eventType] = /* @__PURE__ */ new Set();
544
+ }
545
+ this.eventListeners[eventType]?.add(handler);
546
+ }
547
+ removeEventListener(eventType, handler) {
548
+ const handlers = this.eventListeners[eventType];
549
+ if (handlers) {
550
+ this.eventListeners[eventType]?.delete(handler);
551
+ }
552
+ }
553
+ dispatchEvent(eventType, event) {
554
+ const handlers = this.eventListeners[eventType];
555
+ if (handlers) {
556
+ const copy = [...handlers];
557
+ for (const handler of copy) {
558
+ handler(event);
559
+ }
560
+ }
561
+ }
562
+ };
563
+
564
+ // transport/sessionStateMachine/common.ts
565
+ var import_value = require("@sinclair/typebox/value");
566
+ var ERR_CONSUMED = `session state has been consumed and is no longer valid`;
567
+ var StateMachineState = class {
568
+ /*
569
+ * Whether this state has been consumed
570
+ * and we've moved on to another state
571
+ */
572
+ _isConsumed;
573
+ close() {
574
+ this._handleClose();
575
+ }
576
+ constructor() {
577
+ this._isConsumed = false;
578
+ return new Proxy(this, {
579
+ get(target, prop) {
580
+ if (prop === "_isConsumed" || prop === "id" || prop === "state") {
581
+ return Reflect.get(target, prop);
582
+ }
583
+ if (prop === "_handleStateExit") {
584
+ return () => {
585
+ target._isConsumed = true;
586
+ target._handleStateExit();
587
+ };
588
+ }
589
+ if (prop === "_handleClose") {
590
+ return () => {
591
+ target._handleStateExit();
592
+ target._handleClose();
593
+ };
594
+ }
595
+ if (target._isConsumed) {
596
+ throw new Error(
597
+ `${ERR_CONSUMED}: getting ${prop.toString()} on consumed state`
598
+ );
599
+ }
600
+ return Reflect.get(target, prop);
601
+ },
602
+ set(target, prop, value) {
603
+ if (target._isConsumed) {
604
+ throw new Error(
605
+ `${ERR_CONSUMED}: setting ${prop.toString()} on consumed state`
606
+ );
607
+ }
608
+ return Reflect.set(target, prop, value);
609
+ }
610
+ });
611
+ }
612
+ };
613
+ var CommonSession = class extends StateMachineState {
614
+ from;
615
+ options;
616
+ log;
617
+ constructor(from, options, log) {
618
+ super();
619
+ this.from = from;
620
+ this.options = options;
621
+ this.log = log;
622
+ }
623
+ parseMsg(msg) {
624
+ const parsedMsg = this.options.codec.fromBuffer(msg);
625
+ if (parsedMsg === null) {
626
+ const decodedBuffer = new TextDecoder().decode(Buffer.from(msg));
627
+ this.log?.error(
628
+ `received malformed msg: ${decodedBuffer}`,
629
+ this.loggingMetadata
630
+ );
631
+ return null;
632
+ }
633
+ if (!import_value.Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
634
+ this.log?.error(`received invalid msg: ${JSON.stringify(parsedMsg)}`, {
635
+ ...this.loggingMetadata,
636
+ validationErrors: [
637
+ ...import_value.Value.Errors(OpaqueTransportMessageSchema, parsedMsg)
638
+ ]
639
+ });
640
+ return null;
641
+ }
642
+ return parsedMsg;
643
+ }
644
+ };
645
+ var IdentifiedSession = class extends CommonSession {
646
+ id;
647
+ telemetry;
648
+ to;
649
+ protocolVersion;
650
+ /**
651
+ * Index of the message we will send next (excluding handshake)
652
+ */
653
+ seq;
654
+ /**
655
+ * Number of unique messages we've received this session (excluding handshake)
656
+ */
657
+ ack;
658
+ sendBuffer;
659
+ constructor(id, from, to, seq, ack, sendBuffer, telemetry, options, protocolVersion, log) {
660
+ super(from, options, log);
661
+ this.id = id;
662
+ this.to = to;
663
+ this.seq = seq;
664
+ this.ack = ack;
665
+ this.sendBuffer = sendBuffer;
666
+ this.telemetry = telemetry;
667
+ this.log = log;
668
+ this.protocolVersion = protocolVersion;
669
+ }
670
+ get loggingMetadata() {
671
+ const spanContext = this.telemetry.span.spanContext();
672
+ return {
673
+ clientId: this.from,
674
+ connectedTo: this.to,
675
+ sessionId: this.id,
676
+ telemetry: {
677
+ traceId: spanContext.traceId,
678
+ spanId: spanContext.spanId
679
+ }
680
+ };
681
+ }
682
+ constructMsg(partialMsg) {
683
+ const msg = {
684
+ ...partialMsg,
685
+ id: generateId(),
686
+ to: this.to,
687
+ from: this.from,
688
+ seq: this.seq,
689
+ ack: this.ack
690
+ };
691
+ this.seq++;
692
+ return msg;
693
+ }
694
+ nextSeq() {
695
+ return this.sendBuffer.length > 0 ? this.sendBuffer[0].seq : this.seq;
696
+ }
697
+ send(msg) {
698
+ const constructedMsg = this.constructMsg(msg);
699
+ this.sendBuffer.push(constructedMsg);
700
+ return constructedMsg.id;
701
+ }
702
+ _handleStateExit() {
703
+ }
704
+ _handleClose() {
705
+ this.sendBuffer.length = 0;
706
+ this.telemetry.span.end();
707
+ }
708
+ };
709
+
710
+ // transport/sessionStateMachine/SessionConnecting.ts
711
+ var SessionConnecting = class extends IdentifiedSession {
712
+ state = "Connecting" /* Connecting */;
713
+ connPromise;
714
+ listeners;
715
+ connectionTimeout;
716
+ constructor(connPromise, listeners, ...args) {
717
+ super(...args);
718
+ this.connPromise = connPromise;
719
+ this.listeners = listeners;
720
+ this.connectionTimeout = setTimeout(() => {
721
+ listeners.onConnectionTimeout();
722
+ }, this.options.connectionTimeoutMs);
723
+ connPromise.then(
724
+ (conn) => {
725
+ if (this._isConsumed)
726
+ return;
727
+ listeners.onConnectionEstablished(conn);
728
+ },
729
+ (err) => {
730
+ if (this._isConsumed)
731
+ return;
732
+ listeners.onConnectionFailed(err);
733
+ }
734
+ );
735
+ }
736
+ // close a pending connection if it resolves, ignore errors if the promise
737
+ // ends up rejected anyways
738
+ bestEffortClose() {
739
+ void this.connPromise.then((conn) => conn.close()).catch(() => {
740
+ });
741
+ }
742
+ _handleStateExit() {
743
+ super._handleStateExit();
744
+ clearTimeout(this.connectionTimeout);
745
+ this.connectionTimeout = void 0;
746
+ }
747
+ _handleClose() {
748
+ this.bestEffortClose();
749
+ super._handleClose();
750
+ }
751
+ };
752
+
753
+ // transport/sessionStateMachine/SessionNoConnection.ts
754
+ var SessionNoConnection = class extends IdentifiedSession {
755
+ state = "NoConnection" /* NoConnection */;
756
+ listeners;
757
+ gracePeriodTimeout;
758
+ constructor(listeners, ...args) {
759
+ super(...args);
760
+ this.listeners = listeners;
761
+ this.gracePeriodTimeout = setTimeout(() => {
762
+ this.listeners.onSessionGracePeriodElapsed();
763
+ }, this.options.sessionDisconnectGraceMs);
764
+ }
765
+ _handleClose() {
766
+ super._handleClose();
767
+ }
768
+ _handleStateExit() {
769
+ super._handleStateExit();
770
+ if (this.gracePeriodTimeout) {
771
+ clearTimeout(this.gracePeriodTimeout);
772
+ this.gracePeriodTimeout = void 0;
773
+ }
774
+ }
775
+ };
776
+
777
+ // tracing/index.ts
778
+ var import_api = require("@opentelemetry/api");
779
+
780
+ // package.json
781
+ var version = "0.200.0-rc.4";
782
+
783
+ // tracing/index.ts
784
+ function getPropagationContext(ctx) {
785
+ const tracing = {
786
+ traceparent: "",
787
+ tracestate: ""
788
+ };
789
+ import_api.propagation.inject(ctx, tracing);
790
+ return tracing;
791
+ }
792
+ function createSessionTelemetryInfo(sessionId, to, from, propagationCtx) {
793
+ const parentCtx = propagationCtx ? import_api.propagation.extract(import_api.context.active(), propagationCtx) : import_api.context.active();
794
+ const span = tracer.startSpan(
795
+ `session ${sessionId}`,
796
+ {
797
+ attributes: {
798
+ component: "river",
799
+ "river.session.id": sessionId,
800
+ "river.session.to": to,
801
+ "river.session.from": from
802
+ }
803
+ },
804
+ parentCtx
805
+ );
806
+ const ctx = import_api.trace.setSpan(parentCtx, span);
807
+ return { span, ctx };
808
+ }
809
+ var tracer = import_api.trace.getTracer("river", version);
810
+ var tracing_default = tracer;
811
+
812
+ // transport/sessionStateMachine/SessionWaitingForHandshake.ts
813
+ var SessionWaitingForHandshake = class extends CommonSession {
814
+ state = "WaitingForHandshake" /* WaitingForHandshake */;
815
+ conn;
816
+ listeners;
817
+ handshakeTimeout;
818
+ constructor(conn, listeners, ...args) {
819
+ super(...args);
820
+ this.conn = conn;
821
+ this.listeners = listeners;
822
+ this.handshakeTimeout = setTimeout(() => {
823
+ listeners.onHandshakeTimeout();
824
+ }, this.options.handshakeTimeoutMs);
825
+ this.conn.addDataListener(this.onHandshakeData);
826
+ this.conn.addErrorListener(listeners.onConnectionErrored);
827
+ this.conn.addCloseListener(listeners.onConnectionClosed);
828
+ }
829
+ onHandshakeData = (msg) => {
830
+ const parsedMsg = this.parseMsg(msg);
831
+ if (parsedMsg === null) {
832
+ this.listeners.onInvalidHandshake("could not parse message");
833
+ return;
834
+ }
835
+ this.listeners.onHandshake(parsedMsg);
836
+ };
837
+ get loggingMetadata() {
838
+ return {
839
+ clientId: this.from,
840
+ connId: this.conn.id
841
+ };
842
+ }
843
+ sendHandshake(msg) {
844
+ return this.conn.send(this.options.codec.toBuffer(msg));
780
845
  }
781
- error(msg, metadata) {
782
- if (LoggingLevels[this.minLevel] <= LoggingLevels.error) {
783
- this.output(msg, metadata ?? {}, "error");
784
- }
846
+ _handleStateExit() {
847
+ this.conn.removeDataListener(this.onHandshakeData);
848
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
849
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
850
+ clearTimeout(this.handshakeTimeout);
851
+ this.handshakeTimeout = void 0;
852
+ }
853
+ _handleClose() {
854
+ this.conn.close();
785
855
  }
786
856
  };
787
- var createLogProxy = (log) => ({
788
- debug: cleanedLogFn(log.debug.bind(log)),
789
- info: cleanedLogFn(log.info.bind(log)),
790
- warn: cleanedLogFn(log.warn.bind(log)),
791
- error: cleanedLogFn(log.error.bind(log))
792
- });
793
857
 
794
- // transport/events.ts
795
- var ProtocolError = {
796
- RetriesExceeded: "conn_retry_exceeded",
797
- HandshakeFailed: "handshake_failed",
798
- MessageOrderingViolated: "message_ordering_violated"
799
- };
800
- var EventDispatcher = class {
801
- eventListeners = {};
802
- removeAllListeners() {
803
- this.eventListeners = {};
804
- }
805
- numberOfListeners(eventType) {
806
- return this.eventListeners[eventType]?.size ?? 0;
807
- }
808
- addEventListener(eventType, handler) {
809
- if (!this.eventListeners[eventType]) {
810
- this.eventListeners[eventType] = /* @__PURE__ */ new Set();
858
+ // transport/sessionStateMachine/SessionHandshaking.ts
859
+ var SessionHandshaking = class extends IdentifiedSession {
860
+ state = "Handshaking" /* Handshaking */;
861
+ conn;
862
+ listeners;
863
+ handshakeTimeout;
864
+ constructor(conn, listeners, ...args) {
865
+ super(...args);
866
+ this.conn = conn;
867
+ this.listeners = listeners;
868
+ this.handshakeTimeout = setTimeout(() => {
869
+ listeners.onHandshakeTimeout();
870
+ }, this.options.handshakeTimeoutMs);
871
+ this.conn.addDataListener(this.onHandshakeData);
872
+ this.conn.addErrorListener(listeners.onConnectionErrored);
873
+ this.conn.addCloseListener(listeners.onConnectionClosed);
874
+ }
875
+ onHandshakeData = (msg) => {
876
+ const parsedMsg = this.parseMsg(msg);
877
+ if (parsedMsg === null) {
878
+ this.listeners.onInvalidHandshake("could not parse message");
879
+ return;
811
880
  }
812
- this.eventListeners[eventType]?.add(handler);
881
+ this.listeners.onHandshake(parsedMsg);
882
+ };
883
+ sendHandshake(msg) {
884
+ return this.conn.send(this.options.codec.toBuffer(msg));
813
885
  }
814
- removeEventListener(eventType, handler) {
815
- const handlers = this.eventListeners[eventType];
816
- if (handlers) {
817
- this.eventListeners[eventType]?.delete(handler);
818
- }
886
+ _handleStateExit() {
887
+ super._handleStateExit();
888
+ this.conn.removeDataListener(this.onHandshakeData);
889
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
890
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
891
+ clearTimeout(this.handshakeTimeout);
819
892
  }
820
- dispatchEvent(eventType, event) {
821
- const handlers = this.eventListeners[eventType];
822
- if (handlers) {
823
- const copy = [...handlers];
824
- for (const handler of copy) {
825
- handler(event);
826
- }
827
- }
893
+ _handleClose() {
894
+ super._handleClose();
895
+ this.conn.close();
828
896
  }
829
897
  };
830
898
 
831
- // transport/transport.ts
832
- var import_api3 = require("@opentelemetry/api");
833
- var Transport = class {
834
- /**
835
- * The status of the transport.
836
- */
837
- status;
838
- /**
839
- * The {@link Codec} used to encode and decode messages.
840
- */
841
- codec;
842
- /**
843
- * The client ID of this transport.
844
- */
845
- clientId;
846
- /**
847
- * The map of {@link Session}s managed by this transport.
848
- */
849
- sessions;
850
- /**
851
- * The map of {@link Connection}s managed by this transport.
852
- */
853
- get connections() {
854
- return new Map(
855
- [...this.sessions].map(([client, session]) => [client, session.connection]).filter((entry) => entry[1] !== void 0)
856
- );
899
+ // transport/sessionStateMachine/SessionConnected.ts
900
+ var import_api2 = require("@opentelemetry/api");
901
+ var SessionConnected = class extends IdentifiedSession {
902
+ state = "Connected" /* Connected */;
903
+ conn;
904
+ listeners;
905
+ heartbeatHandle;
906
+ heartbeatMisses = 0;
907
+ get isActivelyHeartbeating() {
908
+ return this.heartbeatHandle !== void 0;
857
909
  }
858
- /**
859
- * The event dispatcher for handling events of type EventTypes.
860
- */
861
- eventDispatcher;
862
- /**
863
- * The options for this transport.
864
- */
865
- options;
866
- log;
867
- /**
868
- * Creates a new Transport instance.
869
- * This should also set up {@link onConnect}, and {@link onDisconnect} listeners.
870
- * @param codec The codec used to encode and decode messages.
871
- * @param clientId The client ID of this transport.
872
- */
873
- constructor(clientId, providedOptions) {
874
- this.options = { ...defaultTransportOptions, ...providedOptions };
875
- this.eventDispatcher = new EventDispatcher();
876
- this.sessions = /* @__PURE__ */ new Map();
877
- this.codec = this.options.codec;
878
- this.clientId = clientId;
879
- this.status = "open";
910
+ updateBookkeeping(ack, seq) {
911
+ this.sendBuffer = this.sendBuffer.filter((unacked) => unacked.seq >= ack);
912
+ this.ack = seq + 1;
913
+ this.heartbeatMisses = 0;
880
914
  }
881
- bindLogger(fn, level) {
882
- if (typeof fn === "function") {
883
- this.log = createLogProxy(new BaseLogger(fn, level));
884
- return;
915
+ send(msg) {
916
+ const constructedMsg = this.constructMsg(msg);
917
+ this.sendBuffer.push(constructedMsg);
918
+ this.conn.send(this.options.codec.toBuffer(constructedMsg));
919
+ return constructedMsg.id;
920
+ }
921
+ constructor(conn, listeners, ...args) {
922
+ super(...args);
923
+ this.conn = conn;
924
+ this.listeners = listeners;
925
+ this.conn.addDataListener(this.onMessageData);
926
+ this.conn.addCloseListener(listeners.onConnectionClosed);
927
+ this.conn.addErrorListener(listeners.onConnectionErrored);
928
+ if (this.sendBuffer.length > 0) {
929
+ this.log?.debug(
930
+ `sending ${this.sendBuffer.length} buffered messages`,
931
+ this.loggingMetadata
932
+ );
933
+ }
934
+ for (const msg of this.sendBuffer) {
935
+ conn.send(this.options.codec.toBuffer(msg));
885
936
  }
886
- this.log = createLogProxy(fn);
887
937
  }
888
- /**
889
- * Called when a new connection is established
890
- * and we know the identity of the connected client.
891
- * @param conn The connection object.
892
- */
893
- onConnect(conn, session, isTransparentReconnect) {
894
- this.eventDispatcher.dispatchEvent("connectionStatus", {
895
- status: "connect",
896
- conn
897
- });
898
- conn.telemetry = createConnectionTelemetryInfo(conn, session.telemetry);
899
- session.replaceWithNewConnection(conn, isTransparentReconnect);
900
- this.log?.info(`connected to ${session.to}`, {
901
- ...conn.loggingMetadata,
902
- ...session.loggingMetadata
938
+ startActiveHeartbeat() {
939
+ this.heartbeatHandle = setInterval(() => {
940
+ const misses = this.heartbeatMisses;
941
+ const missDuration = misses * this.options.heartbeatIntervalMs;
942
+ if (misses >= this.options.heartbeatsUntilDead) {
943
+ this.log?.info(
944
+ `closing connection to ${this.to} due to inactivity (missed ${misses} heartbeats which is ${missDuration}ms)`,
945
+ this.loggingMetadata
946
+ );
947
+ this.telemetry.span.addEvent("closing connection due to inactivity");
948
+ this.conn.close();
949
+ clearInterval(this.heartbeatHandle);
950
+ this.heartbeatHandle = void 0;
951
+ return;
952
+ }
953
+ this.sendHeartbeat();
954
+ this.heartbeatMisses++;
955
+ }, this.options.heartbeatIntervalMs);
956
+ }
957
+ sendHeartbeat() {
958
+ this.send({
959
+ streamId: "heartbeat",
960
+ controlFlags: 1 /* AckBit */,
961
+ payload: {
962
+ type: "ACK"
963
+ }
903
964
  });
904
965
  }
905
- createSession(to, conn, propagationCtx) {
906
- const session = new Session(
907
- conn,
908
- this.clientId,
909
- to,
910
- this.options,
911
- propagationCtx
912
- );
913
- if (this.log) {
914
- session.bindLogger(this.log);
915
- }
916
- const currentSession = this.sessions.get(session.to);
917
- if (currentSession) {
918
- this.log?.warn(
919
- `session ${session.id} from ${session.to} surreptitiously replacing ${currentSession.id}`,
920
- {
921
- ...currentSession.loggingMetadata,
966
+ onMessageData = (msg) => {
967
+ const parsedMsg = this.parseMsg(msg);
968
+ if (parsedMsg === null)
969
+ return;
970
+ if (parsedMsg.seq !== this.ack) {
971
+ if (parsedMsg.seq < this.ack) {
972
+ this.log?.debug(
973
+ `received duplicate msg (got seq: ${parsedMsg.seq}, wanted seq: ${this.ack}), discarding`,
974
+ {
975
+ ...this.loggingMetadata,
976
+ transportMessage: parsedMsg
977
+ }
978
+ );
979
+ } else {
980
+ const reason = `received out-of-order msg (got seq: ${parsedMsg.seq}, wanted seq: ${this.ack})`;
981
+ this.log?.error(reason, {
982
+ ...this.loggingMetadata,
983
+ transportMessage: parsedMsg,
922
984
  tags: ["invariant-violation"]
923
- }
924
- );
925
- this.deleteSession({
926
- session: currentSession,
927
- closeHandshakingConnection: false
928
- });
985
+ });
986
+ this.telemetry.span.setStatus({
987
+ code: import_api2.SpanStatusCode.ERROR,
988
+ message: reason
989
+ });
990
+ this.listeners.onInvalidMessage(reason);
991
+ }
992
+ return;
929
993
  }
930
- this.sessions.set(session.to, session);
931
- this.eventDispatcher.dispatchEvent("sessionStatus", {
932
- status: "connect",
933
- session
994
+ this.log?.debug(`received msg`, {
995
+ ...this.loggingMetadata,
996
+ transportMessage: parsedMsg
934
997
  });
935
- return session;
936
- }
937
- createNewSession({
938
- to,
939
- conn,
940
- sessionId,
941
- propagationCtx
942
- }) {
943
- let session = this.sessions.get(to);
944
- if (session !== void 0) {
945
- this.log?.info(
946
- `session for ${to} already exists, replacing it with a new session as requested`,
947
- session.loggingMetadata
948
- );
949
- this.deleteSession({
950
- session,
951
- closeHandshakingConnection: false
952
- });
953
- session = void 0;
998
+ this.updateBookkeeping(parsedMsg.ack, parsedMsg.seq);
999
+ if (!isAck(parsedMsg.controlFlags)) {
1000
+ this.listeners.onMessage(parsedMsg);
1001
+ return;
954
1002
  }
955
- session = this.createSession(to, conn, propagationCtx);
956
- session.advertisedSessionId = sessionId;
957
- this.log?.info(`created new session for ${to}`, session.loggingMetadata);
958
- return session;
959
- }
960
- getExistingSession({
961
- to,
962
- sessionId,
963
- nextExpectedSeq
964
- }) {
965
- const session = this.sessions.get(to);
966
- if (
967
- // reject this request if there was no previous session to replace
968
- session === void 0 || // or if both parties do not agree about the next expected sequence number
969
- !session.nextExpectedSeqInRange(nextExpectedSeq) || // or if both parties do not agree on the advertised session id
970
- session.advertisedSessionId !== sessionId
971
- ) {
972
- return false;
1003
+ this.log?.debug(`discarding msg (ack bit set)`, {
1004
+ ...this.loggingMetadata,
1005
+ transportMessage: parsedMsg
1006
+ });
1007
+ if (!this.isActivelyHeartbeating) {
1008
+ this.sendHeartbeat();
973
1009
  }
974
- this.log?.info(
975
- `reused existing session for ${to}`,
976
- session.loggingMetadata
977
- );
978
- return session;
1010
+ };
1011
+ _handleStateExit() {
1012
+ super._handleStateExit();
1013
+ this.conn.removeDataListener(this.onMessageData);
1014
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
1015
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
1016
+ clearInterval(this.heartbeatHandle);
1017
+ this.heartbeatHandle = void 0;
1018
+ }
1019
+ _handleClose() {
1020
+ super._handleClose();
1021
+ this.conn.close();
979
1022
  }
980
- getOrCreateSession({
981
- to,
982
- conn,
983
- handshakingConn,
984
- sessionId,
985
- propagationCtx
986
- }) {
987
- let session = this.sessions.get(to);
988
- const isReconnect = session !== void 0;
989
- let isTransparentReconnect = isReconnect;
990
- if (session?.advertisedSessionId !== void 0 && sessionId !== void 0 && session.advertisedSessionId !== sessionId) {
991
- this.log?.info(
992
- `session for ${to} already exists but has a different session id (expected: ${session.advertisedSessionId}, got: ${sessionId}), creating a new one`,
993
- session.loggingMetadata
1023
+ };
1024
+
1025
+ // transport/sessionStateMachine/transitions.ts
1026
+ function inheritSharedSession(session) {
1027
+ return [
1028
+ session.id,
1029
+ session.from,
1030
+ session.to,
1031
+ session.seq,
1032
+ session.ack,
1033
+ session.sendBuffer,
1034
+ session.telemetry,
1035
+ session.options,
1036
+ session.protocolVersion,
1037
+ session.log
1038
+ ];
1039
+ }
1040
+ var SessionStateGraph = {
1041
+ entrypoints: {
1042
+ NoConnection(to, from, listeners, options, protocolVersion, log) {
1043
+ const id = `session-${generateId()}`;
1044
+ const telemetry = createSessionTelemetryInfo(id, to, from);
1045
+ const sendBuffer = [];
1046
+ const session = new SessionNoConnection(
1047
+ listeners,
1048
+ id,
1049
+ from,
1050
+ to,
1051
+ 0,
1052
+ 0,
1053
+ sendBuffer,
1054
+ telemetry,
1055
+ options,
1056
+ protocolVersion,
1057
+ log
1058
+ );
1059
+ session.log?.info(`session ${session.id} created in NoConnection state`, {
1060
+ ...session.loggingMetadata,
1061
+ tags: ["state-transition"]
1062
+ });
1063
+ return session;
1064
+ },
1065
+ WaitingForHandshake(from, conn, listeners, options, log) {
1066
+ const session = new SessionWaitingForHandshake(
1067
+ conn,
1068
+ listeners,
1069
+ from,
1070
+ options,
1071
+ log
994
1072
  );
995
- this.deleteSession({
996
- session,
997
- closeHandshakingConnection: handshakingConn !== void 0,
998
- handshakingConn
1073
+ session.log?.info(`session created in WaitingForHandshake state`, {
1074
+ ...session.loggingMetadata,
1075
+ tags: ["state-transition"]
999
1076
  });
1000
- isTransparentReconnect = false;
1001
- session = void 0;
1077
+ return session;
1002
1078
  }
1003
- if (!session) {
1004
- session = this.createSession(to, conn, propagationCtx);
1005
- this.log?.info(
1006
- `no session for ${to}, created a new one`,
1007
- session.loggingMetadata
1079
+ },
1080
+ // All of the transitions 'move'/'consume' the old session and return a new one.
1081
+ // After a session is transitioned, any usage of the old session will throw.
1082
+ transition: {
1083
+ // happy path transitions
1084
+ NoConnectionToConnecting(oldSession, connPromise, listeners) {
1085
+ const carriedState = inheritSharedSession(oldSession);
1086
+ oldSession._handleStateExit();
1087
+ const session = new SessionConnecting(
1088
+ connPromise,
1089
+ listeners,
1090
+ ...carriedState
1008
1091
  );
1009
- }
1010
- if (sessionId !== void 0) {
1011
- session.advertisedSessionId = sessionId;
1012
- }
1013
- if (handshakingConn !== void 0) {
1014
- session.replaceWithNewHandshakingConnection(handshakingConn);
1015
- }
1016
- return { session, isReconnect, isTransparentReconnect };
1017
- }
1018
- deleteSession({
1019
- session,
1020
- closeHandshakingConnection,
1021
- handshakingConn
1022
- }) {
1023
- if (closeHandshakingConnection) {
1024
- session.closeHandshakingConnection(handshakingConn);
1025
- }
1026
- session.close();
1027
- session.telemetry.span.end();
1028
- const currentSession = this.sessions.get(session.to);
1029
- if (currentSession && currentSession.id !== session.id) {
1030
- this.log?.warn(
1031
- `session ${session.id} disconnect from ${session.to}, mismatch with ${currentSession.id}`,
1092
+ session.log?.info(
1093
+ `session ${session.id} transition from NoConnection to Connecting`,
1032
1094
  {
1033
1095
  ...session.loggingMetadata,
1034
- tags: ["invariant-violation"]
1096
+ tags: ["state-transition"]
1035
1097
  }
1036
1098
  );
1037
- return;
1099
+ return session;
1100
+ },
1101
+ ConnectingToHandshaking(oldSession, conn, listeners) {
1102
+ const carriedState = inheritSharedSession(oldSession);
1103
+ oldSession._handleStateExit();
1104
+ const session = new SessionHandshaking(conn, listeners, ...carriedState);
1105
+ session.log?.info(
1106
+ `session ${session.id} transition from Connecting to Handshaking`,
1107
+ {
1108
+ ...session.loggingMetadata,
1109
+ tags: ["state-transition"]
1110
+ }
1111
+ );
1112
+ return session;
1113
+ },
1114
+ HandshakingToConnected(oldSession, listeners) {
1115
+ const carriedState = inheritSharedSession(oldSession);
1116
+ const conn = oldSession.conn;
1117
+ oldSession._handleStateExit();
1118
+ const session = new SessionConnected(conn, listeners, ...carriedState);
1119
+ session.log?.info(
1120
+ `session ${session.id} transition from Handshaking to Connected`,
1121
+ {
1122
+ ...session.loggingMetadata,
1123
+ tags: ["state-transition"]
1124
+ }
1125
+ );
1126
+ return session;
1127
+ },
1128
+ WaitingForHandshakeToConnected(pendingSession, oldSession, sessionId, to, propagationCtx, listeners, protocolVersion) {
1129
+ const conn = pendingSession.conn;
1130
+ const { from, options } = pendingSession;
1131
+ const carriedState = oldSession ? (
1132
+ // old session exists, inherit state
1133
+ inheritSharedSession(oldSession)
1134
+ ) : (
1135
+ // old session does not exist, create new state
1136
+ [
1137
+ sessionId,
1138
+ from,
1139
+ to,
1140
+ 0,
1141
+ 0,
1142
+ [],
1143
+ createSessionTelemetryInfo(sessionId, to, from, propagationCtx),
1144
+ options,
1145
+ protocolVersion,
1146
+ pendingSession.log
1147
+ ]
1148
+ );
1149
+ pendingSession._handleStateExit();
1150
+ oldSession?._handleStateExit();
1151
+ const session = new SessionConnected(conn, listeners, ...carriedState);
1152
+ session.log?.info(
1153
+ `session ${session.id} transition from WaitingForHandshake to Connected`,
1154
+ {
1155
+ ...session.loggingMetadata,
1156
+ tags: ["state-transition"]
1157
+ }
1158
+ );
1159
+ return session;
1160
+ },
1161
+ // disconnect paths
1162
+ ConnectingToNoConnection(oldSession, listeners) {
1163
+ const carriedState = inheritSharedSession(oldSession);
1164
+ oldSession.bestEffortClose();
1165
+ oldSession._handleStateExit();
1166
+ const session = new SessionNoConnection(listeners, ...carriedState);
1167
+ session.log?.info(
1168
+ `session ${session.id} transition from Connecting to NoConnection`,
1169
+ {
1170
+ ...session.loggingMetadata,
1171
+ tags: ["state-transition"]
1172
+ }
1173
+ );
1174
+ return session;
1175
+ },
1176
+ HandshakingToNoConnection(oldSession, listeners) {
1177
+ const carriedState = inheritSharedSession(oldSession);
1178
+ oldSession.conn.close();
1179
+ oldSession._handleStateExit();
1180
+ const session = new SessionNoConnection(listeners, ...carriedState);
1181
+ session.log?.info(
1182
+ `session ${session.id} transition from Handshaking to NoConnection`,
1183
+ {
1184
+ ...session.loggingMetadata,
1185
+ tags: ["state-transition"]
1186
+ }
1187
+ );
1188
+ return session;
1189
+ },
1190
+ ConnectedToNoConnection(oldSession, listeners) {
1191
+ const carriedState = inheritSharedSession(oldSession);
1192
+ oldSession.conn.close();
1193
+ oldSession._handleStateExit();
1194
+ const session = new SessionNoConnection(listeners, ...carriedState);
1195
+ session.log?.info(
1196
+ `session ${session.id} transition from Connected to NoConnection`,
1197
+ {
1198
+ ...session.loggingMetadata,
1199
+ tags: ["state-transition"]
1200
+ }
1201
+ );
1202
+ return session;
1038
1203
  }
1039
- this.sessions.delete(session.to);
1040
- this.log?.info(
1041
- `session ${session.id} disconnect from ${session.to}`,
1042
- session.loggingMetadata
1043
- );
1044
- this.eventDispatcher.dispatchEvent("sessionStatus", {
1045
- status: "disconnect",
1046
- session
1047
- });
1048
1204
  }
1205
+ };
1206
+
1207
+ // transport/transport.ts
1208
+ var Transport = class {
1049
1209
  /**
1050
- * The downstream implementation needs to call this when a connection is closed.
1051
- * @param conn The connection object.
1052
- * @param connectedTo The peer we are connected to.
1210
+ * The status of the transport.
1053
1211
  */
1054
- onDisconnect(conn, session) {
1055
- if (session.connection !== void 0 && session.connection.id !== conn.id) {
1056
- session.telemetry.span.addEvent("onDisconnect race");
1057
- this.log?.warn("onDisconnect race", {
1058
- clientId: this.clientId,
1059
- ...session.loggingMetadata,
1060
- ...conn.loggingMetadata,
1061
- tags: ["invariant-violation"]
1062
- });
1063
- return;
1064
- }
1065
- conn.telemetry?.span.end();
1066
- this.eventDispatcher.dispatchEvent("connectionStatus", {
1067
- status: "disconnect",
1068
- conn
1069
- });
1070
- session.connection = void 0;
1071
- session.beginGrace(() => {
1072
- if (session.connection !== void 0) {
1073
- session.telemetry.span.addEvent("session grace period race");
1074
- this.log?.warn("session grace period race", {
1075
- clientId: this.clientId,
1076
- ...session.loggingMetadata,
1077
- ...conn.loggingMetadata,
1078
- tags: ["invariant-violation"]
1079
- });
1080
- return;
1081
- }
1082
- session.telemetry.span.addEvent("session grace period expired");
1083
- this.deleteSession({
1084
- session,
1085
- closeHandshakingConnection: true,
1086
- handshakingConn: conn
1087
- });
1088
- });
1089
- }
1212
+ status;
1213
+ /**
1214
+ * The client ID of this transport.
1215
+ */
1216
+ clientId;
1090
1217
  /**
1091
- * Parses a message from a Uint8Array into a {@link OpaqueTransportMessage}.
1092
- * @param msg The message to parse.
1093
- * @returns The parsed message, or null if the message is malformed or invalid.
1218
+ * The event dispatcher for handling events of type EventTypes.
1094
1219
  */
1095
- parseMsg(msg, conn) {
1096
- const parsedMsg = this.codec.fromBuffer(msg);
1097
- if (parsedMsg === null) {
1098
- const decodedBuffer = new TextDecoder().decode(Buffer.from(msg));
1099
- this.log?.error(
1100
- `received malformed msg, killing conn: ${decodedBuffer}`,
1101
- {
1102
- clientId: this.clientId,
1103
- ...conn.loggingMetadata
1104
- }
1105
- );
1106
- return null;
1107
- }
1108
- if (!import_value.Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
1109
- this.log?.error(`received invalid msg: ${JSON.stringify(parsedMsg)}`, {
1110
- clientId: this.clientId,
1111
- ...conn.loggingMetadata,
1112
- validationErrors: [
1113
- ...import_value.Value.Errors(OpaqueTransportMessageSchema, parsedMsg)
1114
- ]
1115
- });
1116
- return null;
1220
+ eventDispatcher;
1221
+ /**
1222
+ * The options for this transport.
1223
+ */
1224
+ options;
1225
+ log;
1226
+ sessions;
1227
+ /**
1228
+ * Creates a new Transport instance.
1229
+ * @param codec The codec used to encode and decode messages.
1230
+ * @param clientId The client ID of this transport.
1231
+ */
1232
+ constructor(clientId, providedOptions) {
1233
+ this.options = { ...defaultTransportOptions, ...providedOptions };
1234
+ this.eventDispatcher = new EventDispatcher();
1235
+ this.clientId = clientId;
1236
+ this.status = "open";
1237
+ this.sessions = /* @__PURE__ */ new Map();
1238
+ }
1239
+ bindLogger(fn, level) {
1240
+ if (typeof fn === "function") {
1241
+ this.log = createLogProxy(new BaseLogger(fn, level));
1242
+ return;
1117
1243
  }
1118
- return parsedMsg;
1244
+ this.log = createLogProxy(fn);
1119
1245
  }
1120
1246
  /**
1121
1247
  * Called when a message is received by this transport.
1122
1248
  * You generally shouldn't need to override this in downstream transport implementations.
1123
1249
  * @param msg The received message.
1124
1250
  */
1125
- handleMsg(msg, conn) {
1251
+ handleMsg(msg) {
1126
1252
  if (this.getStatus() !== "open")
1127
1253
  return;
1128
- const session = this.sessions.get(msg.from);
1129
- if (!session) {
1130
- this.log?.error(`received message for unknown session from ${msg.from}`, {
1131
- clientId: this.clientId,
1132
- transportMessage: msg,
1133
- ...conn.loggingMetadata,
1134
- tags: ["invariant-violation"]
1135
- });
1136
- return;
1137
- }
1138
- session.cancelGrace();
1139
- this.log?.debug(`received msg`, {
1140
- clientId: this.clientId,
1141
- transportMessage: msg,
1142
- ...conn.loggingMetadata
1143
- });
1144
- if (msg.seq !== session.nextExpectedSeq) {
1145
- if (msg.seq < session.nextExpectedSeq) {
1146
- this.log?.debug(
1147
- `received duplicate msg (got seq: ${msg.seq}, wanted seq: ${session.nextExpectedSeq}), discarding`,
1148
- {
1149
- clientId: this.clientId,
1150
- transportMessage: msg,
1151
- ...conn.loggingMetadata
1152
- }
1153
- );
1154
- } else {
1155
- const errMsg = `received out-of-order msg (got seq: ${msg.seq}, wanted seq: ${session.nextExpectedSeq})`;
1156
- this.log?.error(`${errMsg}, marking connection as dead`, {
1157
- clientId: this.clientId,
1158
- transportMessage: msg,
1159
- ...conn.loggingMetadata,
1160
- tags: ["invariant-violation"]
1161
- });
1162
- this.protocolError(ProtocolError.MessageOrderingViolated, errMsg);
1163
- session.telemetry.span.setStatus({
1164
- code: import_api3.SpanStatusCode.ERROR,
1165
- message: "message order violated"
1166
- });
1167
- this.deleteSession({ session, closeHandshakingConnection: true });
1168
- }
1169
- return;
1170
- }
1171
- session.updateBookkeeping(msg.ack, msg.seq);
1172
- if (!isAck(msg.controlFlags)) {
1173
- this.eventDispatcher.dispatchEvent("message", msg);
1174
- } else {
1175
- this.log?.debug(`discarding msg (ack bit set)`, {
1176
- clientId: this.clientId,
1177
- transportMessage: msg,
1178
- ...conn.loggingMetadata
1179
- });
1180
- }
1254
+ this.eventDispatcher.dispatchEvent("message", msg);
1181
1255
  }
1182
1256
  /**
1183
1257
  * Adds a listener to this transport.
@@ -1195,50 +1269,6 @@ var Transport = class {
1195
1269
  removeEventListener(type, handler) {
1196
1270
  this.eventDispatcher.removeEventListener(type, handler);
1197
1271
  }
1198
- /**
1199
- * Sends a message over this transport, delegating to the appropriate connection to actually
1200
- * send the message.
1201
- * @param msg The message to send.
1202
- * @returns The ID of the sent message or undefined if it wasn't sent
1203
- */
1204
- send(to, msg) {
1205
- if (this.getStatus() === "closed") {
1206
- const err = "transport is closed, cant send";
1207
- this.log?.error(err, {
1208
- clientId: this.clientId,
1209
- transportMessage: msg,
1210
- tags: ["invariant-violation"]
1211
- });
1212
- throw new Error(err);
1213
- }
1214
- return this.getOrCreateSession({ to }).session.send(msg);
1215
- }
1216
- // control helpers
1217
- sendCloseControl(to, streamId) {
1218
- return this.send(to, {
1219
- streamId,
1220
- controlFlags: 8 /* StreamClosedBit */,
1221
- payload: {
1222
- type: "CLOSE"
1223
- }
1224
- });
1225
- }
1226
- sendRequestCloseControl(to, streamId) {
1227
- return this.send(to, {
1228
- streamId,
1229
- controlFlags: 16 /* StreamCloseRequestBit */,
1230
- payload: {
1231
- type: "CLOSE"
1232
- }
1233
- });
1234
- }
1235
- sendAbort(to, streamId, payload) {
1236
- return this.send(to, {
1237
- streamId,
1238
- controlFlags: 4 /* StreamAbortBit */,
1239
- payload
1240
- });
1241
- }
1242
1272
  protocolError(type, message) {
1243
1273
  this.eventDispatcher.dispatchEvent("protocolError", { type, message });
1244
1274
  }
@@ -1250,7 +1280,7 @@ var Transport = class {
1250
1280
  close() {
1251
1281
  this.status = "closed";
1252
1282
  for (const session of this.sessions.values()) {
1253
- this.deleteSession({ session, closeHandshakingConnection: true });
1283
+ this.deleteSession(session);
1254
1284
  }
1255
1285
  this.eventDispatcher.dispatchEvent("transportStatus", {
1256
1286
  status: this.status
@@ -1261,6 +1291,68 @@ var Transport = class {
1261
1291
  getStatus() {
1262
1292
  return this.status;
1263
1293
  }
1294
+ updateSession(session) {
1295
+ const activeSession = this.sessions.get(session.to);
1296
+ if (activeSession && activeSession.id !== session.id) {
1297
+ const msg = `attempt to transition active session for ${session.to} but active session (${activeSession.id}) is different from handle (${session.id})`;
1298
+ throw new Error(msg);
1299
+ }
1300
+ this.sessions.set(session.to, session);
1301
+ if (!activeSession) {
1302
+ this.eventDispatcher.dispatchEvent("sessionStatus", {
1303
+ status: "connect",
1304
+ session
1305
+ });
1306
+ }
1307
+ this.eventDispatcher.dispatchEvent("sessionTransition", {
1308
+ state: session.state,
1309
+ session
1310
+ });
1311
+ return session;
1312
+ }
1313
+ // state transitions
1314
+ deleteSession(session) {
1315
+ session.log?.info(`closing session ${session.id}`, session.loggingMetadata);
1316
+ this.eventDispatcher.dispatchEvent("sessionStatus", {
1317
+ status: "disconnect",
1318
+ session
1319
+ });
1320
+ session.close();
1321
+ this.sessions.delete(session.to);
1322
+ }
1323
+ // common listeners
1324
+ onSessionGracePeriodElapsed(session) {
1325
+ this.log?.warn(
1326
+ `session to ${session.to} grace period elapsed, closing`,
1327
+ session.loggingMetadata
1328
+ );
1329
+ this.deleteSession(session);
1330
+ }
1331
+ onConnectingFailed(session) {
1332
+ const noConnectionSession = SessionStateGraph.transition.ConnectingToNoConnection(session, {
1333
+ onSessionGracePeriodElapsed: () => {
1334
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1335
+ }
1336
+ });
1337
+ return this.updateSession(noConnectionSession);
1338
+ }
1339
+ onConnClosed(session) {
1340
+ let noConnectionSession;
1341
+ if (session.state === "Handshaking" /* Handshaking */) {
1342
+ noConnectionSession = SessionStateGraph.transition.HandshakingToNoConnection(session, {
1343
+ onSessionGracePeriodElapsed: () => {
1344
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1345
+ }
1346
+ });
1347
+ } else {
1348
+ noConnectionSession = SessionStateGraph.transition.ConnectedToNoConnection(session, {
1349
+ onSessionGracePeriodElapsed: () => {
1350
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1351
+ }
1352
+ });
1353
+ }
1354
+ return this.updateSession(noConnectionSession);
1355
+ }
1264
1356
  };
1265
1357
 
1266
1358
  // util/stringify.ts
@@ -1278,10 +1370,6 @@ var ClientTransport = class extends Transport {
1278
1370
  * The options for this transport.
1279
1371
  */
1280
1372
  options;
1281
- /**
1282
- * The map of reconnect promises for each client ID.
1283
- */
1284
- inflightConnectionPromises;
1285
1373
  retryBudget;
1286
1374
  /**
1287
1375
  * A flag indicating whether the transport should automatically reconnect
@@ -1300,352 +1388,279 @@ var ClientTransport = class extends Transport {
1300
1388
  ...defaultClientTransportOptions,
1301
1389
  ...providedOptions
1302
1390
  };
1303
- this.inflightConnectionPromises = /* @__PURE__ */ new Map();
1304
1391
  this.retryBudget = new LeakyBucketRateLimit(this.options);
1305
1392
  }
1306
1393
  extendHandshake(options) {
1307
1394
  this.handshakeExtensions = options;
1308
1395
  }
1309
- handleConnection(conn, to) {
1310
- if (this.getStatus() !== "open")
1311
- return;
1312
- let session = void 0;
1313
- const handshakeTimeout = setTimeout(() => {
1314
- if (session)
1315
- return;
1316
- this.log?.warn(
1317
- `connection to ${to} timed out waiting for handshake, closing`,
1318
- { ...conn.loggingMetadata, clientId: this.clientId, connectedTo: to }
1319
- );
1320
- conn.close();
1321
- }, this.options.sessionDisconnectGraceMs);
1322
- const handshakeHandler = (data) => {
1323
- const maybeSession = this.receiveHandshakeResponseMessage(data, conn);
1324
- clearTimeout(handshakeTimeout);
1325
- if (!maybeSession) {
1326
- conn.close();
1327
- return;
1328
- } else {
1329
- session = maybeSession;
1330
- }
1331
- conn.removeDataListener(handshakeHandler);
1332
- conn.addDataListener((data2) => {
1333
- const parsed = this.parseMsg(data2, conn);
1334
- if (!parsed) {
1335
- conn.telemetry?.span.setStatus({
1336
- code: import_api4.SpanStatusCode.ERROR,
1337
- message: "message parse failure"
1338
- });
1339
- conn.close();
1340
- return;
1341
- }
1342
- this.handleMsg(parsed, conn);
1396
+ tryReconnecting(to) {
1397
+ if (this.reconnectOnConnectionDrop && this.getStatus() === "open") {
1398
+ this.connect(to);
1399
+ }
1400
+ }
1401
+ send(to, msg) {
1402
+ if (this.getStatus() === "closed") {
1403
+ const err = "transport is closed, cant send";
1404
+ this.log?.error(err, {
1405
+ clientId: this.clientId,
1406
+ transportMessage: msg,
1407
+ tags: ["invariant-violation"]
1343
1408
  });
1344
- };
1345
- conn.addDataListener(handshakeHandler);
1346
- conn.addCloseListener(() => {
1347
- if (session) {
1348
- this.onDisconnect(conn, session);
1349
- }
1350
- const willReconnect = this.reconnectOnConnectionDrop && this.getStatus() === "open";
1351
- this.log?.info(
1352
- `connection to ${to} disconnected` + (willReconnect ? ", reconnecting" : ""),
1353
- {
1354
- ...conn.loggingMetadata,
1355
- ...session?.loggingMetadata,
1356
- clientId: this.clientId,
1357
- connectedTo: to
1409
+ throw new Error(err);
1410
+ }
1411
+ let session = this.sessions.get(to);
1412
+ if (!session) {
1413
+ session = this.createUnconnectedSession(to);
1414
+ }
1415
+ return session.send(msg);
1416
+ }
1417
+ createUnconnectedSession(to) {
1418
+ const session = SessionStateGraph.entrypoints.NoConnection(
1419
+ to,
1420
+ this.clientId,
1421
+ {
1422
+ onSessionGracePeriodElapsed: () => {
1423
+ this.onSessionGracePeriodElapsed(session);
1358
1424
  }
1359
- );
1360
- this.inflightConnectionPromises.delete(to);
1361
- if (this.reconnectOnConnectionDrop) {
1362
- void this.connect(to);
1425
+ },
1426
+ this.options,
1427
+ currentProtocolVersion,
1428
+ this.log
1429
+ );
1430
+ this.updateSession(session);
1431
+ return session;
1432
+ }
1433
+ // listeners
1434
+ onConnectingFailed(session) {
1435
+ const noConnectionSession = super.onConnectingFailed(session);
1436
+ this.tryReconnecting(noConnectionSession.to);
1437
+ return noConnectionSession;
1438
+ }
1439
+ onConnClosed(session) {
1440
+ const noConnectionSession = super.onConnClosed(session);
1441
+ this.tryReconnecting(noConnectionSession.to);
1442
+ return noConnectionSession;
1443
+ }
1444
+ onConnectionEstablished(session, conn) {
1445
+ const handshakingSession = SessionStateGraph.transition.ConnectingToHandshaking(session, conn, {
1446
+ onConnectionErrored: (err) => {
1447
+ const errStr = coerceErrorString(err);
1448
+ this.log?.error(
1449
+ `connection to ${handshakingSession.to} errored during handshake: ${errStr}`,
1450
+ handshakingSession.loggingMetadata
1451
+ );
1452
+ },
1453
+ onConnectionClosed: () => {
1454
+ this.log?.warn(
1455
+ `connection to ${handshakingSession.to} closed during handshake`,
1456
+ handshakingSession.loggingMetadata
1457
+ );
1458
+ this.onConnClosed(handshakingSession);
1459
+ },
1460
+ onHandshake: (msg) => {
1461
+ this.onHandshakeResponse(handshakingSession, msg);
1462
+ },
1463
+ onInvalidHandshake: (reason) => {
1464
+ this.log?.error(
1465
+ `invalid handshake: ${reason}`,
1466
+ handshakingSession.loggingMetadata
1467
+ );
1468
+ this.deleteSession(session);
1469
+ this.protocolError(ProtocolError.HandshakeFailed, reason);
1470
+ },
1471
+ onHandshakeTimeout: () => {
1472
+ this.log?.error(
1473
+ `connection to ${handshakingSession.to} timed out during handshake`,
1474
+ handshakingSession.loggingMetadata
1475
+ );
1476
+ this.onConnClosed(handshakingSession);
1363
1477
  }
1364
1478
  });
1365
- conn.addErrorListener((err) => {
1366
- conn.telemetry?.span.setStatus({
1367
- code: import_api4.SpanStatusCode.ERROR,
1368
- message: "connection error"
1369
- });
1370
- this.log?.warn(
1371
- `error in connection to ${to}: ${coerceErrorString(err)}`,
1372
- {
1373
- ...conn.loggingMetadata,
1374
- ...session?.loggingMetadata,
1375
- clientId: this.clientId,
1376
- connectedTo: to
1377
- }
1378
- );
1479
+ this.updateSession(handshakingSession);
1480
+ void this.sendHandshake(handshakingSession);
1481
+ return handshakingSession;
1482
+ }
1483
+ rejectHandshakeResponse(session, reason, metadata) {
1484
+ session.conn.telemetry?.span.setStatus({
1485
+ code: import_api3.SpanStatusCode.ERROR,
1486
+ message: reason
1379
1487
  });
1488
+ this.log?.warn(reason, metadata);
1489
+ this.deleteSession(session);
1380
1490
  }
1381
- receiveHandshakeResponseMessage(data, conn) {
1382
- const parsed = this.parseMsg(data, conn);
1383
- if (!parsed) {
1384
- conn.telemetry?.span.setStatus({
1385
- code: import_api4.SpanStatusCode.ERROR,
1386
- message: "non-transport message"
1387
- });
1388
- this.protocolError(
1389
- ProtocolError.HandshakeFailed,
1390
- "received non-transport message"
1391
- );
1392
- return false;
1393
- }
1394
- if (!import_value2.Value.Check(ControlMessageHandshakeResponseSchema, parsed.payload)) {
1395
- conn.telemetry?.span.setStatus({
1396
- code: import_api4.SpanStatusCode.ERROR,
1397
- message: "invalid handshake response"
1398
- });
1399
- this.log?.warn(`received invalid handshake resp`, {
1400
- ...conn.loggingMetadata,
1401
- clientId: this.clientId,
1402
- connectedTo: parsed.from,
1403
- transportMessage: parsed,
1491
+ onHandshakeResponse(session, msg) {
1492
+ if (!import_value2.Value.Check(ControlMessageHandshakeResponseSchema, msg.payload)) {
1493
+ const reason = `received invalid handshake response`;
1494
+ this.rejectHandshakeResponse(session, reason, {
1495
+ ...session.loggingMetadata,
1496
+ transportMessage: msg,
1404
1497
  validationErrors: [
1405
- ...import_value2.Value.Errors(
1406
- ControlMessageHandshakeResponseSchema,
1407
- parsed.payload
1408
- )
1498
+ ...import_value2.Value.Errors(ControlMessageHandshakeResponseSchema, msg.payload)
1409
1499
  ]
1410
1500
  });
1411
- this.protocolError(
1412
- ProtocolError.HandshakeFailed,
1413
- "invalid handshake resp"
1414
- );
1415
- return false;
1501
+ return;
1416
1502
  }
1417
- const previousSession = this.sessions.get(parsed.from);
1418
- if (!parsed.payload.status.ok) {
1419
- if (parsed.payload.status.reason === SESSION_STATE_MISMATCH) {
1420
- if (previousSession) {
1421
- this.deleteSession({
1422
- session: previousSession,
1423
- closeHandshakingConnection: true
1424
- });
1425
- }
1426
- conn.telemetry?.span.setStatus({
1427
- code: import_api4.SpanStatusCode.ERROR,
1428
- message: parsed.payload.status.reason
1429
- });
1503
+ if (!msg.payload.status.ok) {
1504
+ const retriable = msg.payload.status.code ? import_value2.Value.Check(
1505
+ HandshakeErrorRetriableResponseCodes,
1506
+ msg.payload.status.code
1507
+ ) : false;
1508
+ const reason = `handshake failed: ${msg.payload.status.reason}`;
1509
+ this.rejectHandshakeResponse(session, reason, {
1510
+ ...session.loggingMetadata,
1511
+ transportMessage: msg
1512
+ });
1513
+ if (retriable) {
1514
+ this.tryReconnecting(session.to);
1430
1515
  } else {
1431
- conn.telemetry?.span.setStatus({
1432
- code: import_api4.SpanStatusCode.ERROR,
1433
- message: "handshake rejected"
1434
- });
1516
+ this.deleteSession(session);
1517
+ this.protocolError(ProtocolError.HandshakeFailed, reason);
1435
1518
  }
1436
- this.log?.warn(
1437
- `received handshake rejection: ${parsed.payload.status.reason}`,
1438
- {
1439
- ...conn.loggingMetadata,
1440
- clientId: this.clientId,
1441
- connectedTo: parsed.from,
1442
- transportMessage: parsed
1443
- }
1444
- );
1445
- this.protocolError(
1446
- ProtocolError.HandshakeFailed,
1447
- parsed.payload.status.reason
1448
- );
1449
- return false;
1519
+ return;
1450
1520
  }
1451
- if (previousSession?.advertisedSessionId && previousSession.advertisedSessionId !== parsed.payload.status.sessionId) {
1452
- this.deleteSession({
1453
- session: previousSession,
1454
- closeHandshakingConnection: true
1455
- });
1456
- conn.telemetry?.span.setStatus({
1457
- code: import_api4.SpanStatusCode.ERROR,
1458
- message: "session id mismatch"
1459
- });
1460
- this.log?.warn(`handshake from ${parsed.from} session id mismatch`, {
1461
- ...conn.loggingMetadata,
1462
- clientId: this.clientId,
1463
- connectedTo: parsed.from,
1464
- transportMessage: parsed
1521
+ if (msg.payload.status.sessionId !== session.id) {
1522
+ const reason = `session id mismatch: expected ${session.id}, got ${msg.payload.status.sessionId}`;
1523
+ this.rejectHandshakeResponse(session, reason, {
1524
+ ...session.loggingMetadata,
1525
+ transportMessage: msg
1465
1526
  });
1466
- this.protocolError(ProtocolError.HandshakeFailed, "session id mismatch");
1467
- return false;
1527
+ return;
1468
1528
  }
1469
- this.log?.debug(`handshake from ${parsed.from} ok`, {
1470
- ...conn.loggingMetadata,
1471
- clientId: this.clientId,
1472
- connectedTo: parsed.from,
1473
- transportMessage: parsed
1529
+ this.log?.info(`handshake from ${msg.from} ok`, {
1530
+ ...session.loggingMetadata,
1531
+ transportMessage: msg
1474
1532
  });
1475
- const { session, isTransparentReconnect } = this.getOrCreateSession({
1476
- to: parsed.from,
1477
- conn,
1478
- sessionId: parsed.payload.status.sessionId
1533
+ const connectedSession = SessionStateGraph.transition.HandshakingToConnected(session, {
1534
+ onConnectionErrored: (err) => {
1535
+ const errStr = coerceErrorString(err);
1536
+ this.log?.warn(
1537
+ `connection to ${connectedSession.to} errored: ${errStr}`,
1538
+ connectedSession.loggingMetadata
1539
+ );
1540
+ },
1541
+ onConnectionClosed: () => {
1542
+ this.log?.info(
1543
+ `connection to ${connectedSession.to} closed`,
1544
+ connectedSession.loggingMetadata
1545
+ );
1546
+ this.onConnClosed(connectedSession);
1547
+ },
1548
+ onMessage: (msg2) => this.handleMsg(msg2),
1549
+ onInvalidMessage: (reason) => {
1550
+ this.deleteSession(connectedSession);
1551
+ this.protocolError(ProtocolError.MessageOrderingViolated, reason);
1552
+ }
1479
1553
  });
1480
- this.onConnect(conn, session, isTransparentReconnect);
1481
- this.retryBudget.startRestoringBudget(session.to);
1482
- return session;
1554
+ this.updateSession(connectedSession);
1555
+ this.retryBudget.startRestoringBudget(connectedSession.to);
1483
1556
  }
1484
1557
  /**
1485
1558
  * Manually attempts to connect to a client.
1486
1559
  * @param to The client ID of the node to connect to.
1487
1560
  */
1488
- async connect(to) {
1489
- if (this.connections.has(to)) {
1490
- this.log?.info(`already connected to ${to}, skipping connect attempt`, {
1491
- clientId: this.clientId,
1492
- connectedTo: to
1493
- });
1561
+ connect(to) {
1562
+ let session = this.sessions.get(to);
1563
+ session ??= this.createUnconnectedSession(to);
1564
+ if (session.state !== "NoConnection" /* NoConnection */) {
1565
+ this.log?.debug(
1566
+ `session to ${to} has state ${session.state}, skipping connect attempt`,
1567
+ session.loggingMetadata
1568
+ );
1494
1569
  return;
1495
1570
  }
1496
- const canProceedWithConnection = () => this.getStatus() === "open";
1497
- if (!canProceedWithConnection()) {
1571
+ if (this.getStatus() !== "open") {
1498
1572
  this.log?.info(
1499
1573
  `transport state is no longer open, cancelling attempt to connect to ${to}`,
1500
- { clientId: this.clientId, connectedTo: to }
1574
+ session.loggingMetadata
1501
1575
  );
1502
1576
  return;
1503
1577
  }
1504
- let reconnectPromise = this.inflightConnectionPromises.get(to);
1505
- if (!reconnectPromise) {
1506
- if (!this.retryBudget.hasBudget(to)) {
1507
- const budgetConsumed = this.retryBudget.getBudgetConsumed(to);
1508
- const errMsg = `tried to connect to ${to} but retry budget exceeded (more than ${budgetConsumed} attempts in the last ${this.retryBudget.totalBudgetRestoreTime}ms)`;
1509
- this.log?.error(errMsg, { clientId: this.clientId, connectedTo: to });
1510
- this.protocolError(ProtocolError.RetriesExceeded, errMsg);
1511
- return;
1512
- }
1513
- let sleep = Promise.resolve();
1514
- const backoffMs = this.retryBudget.getBackoffMs(to);
1515
- if (backoffMs > 0) {
1516
- sleep = new Promise((resolve) => setTimeout(resolve, backoffMs));
1517
- }
1518
- this.log?.info(
1519
- `attempting connection to ${to} (${backoffMs}ms backoff)`,
1520
- {
1521
- clientId: this.clientId,
1522
- connectedTo: to
1523
- }
1524
- );
1525
- this.retryBudget.consumeBudget(to);
1526
- reconnectPromise = tracing_default.startActiveSpan("connect", async (span) => {
1527
- try {
1528
- span.addEvent("backoff", { backoffMs });
1529
- await sleep;
1530
- if (!canProceedWithConnection()) {
1531
- throw new Error("transport state is no longer open");
1532
- }
1533
- span.addEvent("connecting");
1534
- const conn = await this.createNewOutgoingConnection(to);
1535
- if (!canProceedWithConnection()) {
1536
- this.log?.info(
1537
- `transport state is no longer open, closing pre-handshake connection to ${to}`,
1538
- {
1539
- ...conn.loggingMetadata,
1540
- clientId: this.clientId,
1541
- connectedTo: to
1542
- }
1543
- );
1544
- conn.close();
1545
- throw new Error("transport state is no longer open");
1546
- }
1547
- span.addEvent("sending handshake");
1548
- const ok = await this.sendHandshake(to, conn);
1549
- if (!ok) {
1550
- conn.close();
1551
- throw new Error("failed to send handshake");
1552
- }
1553
- return conn;
1554
- } catch (err) {
1555
- const errStr = coerceErrorString(err);
1556
- span.recordException(errStr);
1557
- span.setStatus({ code: import_api4.SpanStatusCode.ERROR });
1558
- throw err;
1559
- } finally {
1560
- span.end();
1561
- }
1562
- });
1563
- this.inflightConnectionPromises.set(to, reconnectPromise);
1564
- } else {
1565
- this.log?.info(
1566
- `attempting connection to ${to} (reusing previous attempt)`,
1567
- {
1568
- clientId: this.clientId,
1569
- connectedTo: to
1570
- }
1571
- );
1578
+ if (!this.retryBudget.hasBudget(to)) {
1579
+ const budgetConsumed = this.retryBudget.getBudgetConsumed(to);
1580
+ const errMsg = `tried to connect to ${to} but retry budget exceeded (more than ${budgetConsumed} attempts in the last ${this.retryBudget.totalBudgetRestoreTime}ms)`;
1581
+ this.log?.error(errMsg, session.loggingMetadata);
1582
+ this.protocolError(ProtocolError.RetriesExceeded, errMsg);
1583
+ return;
1572
1584
  }
1573
- try {
1574
- await reconnectPromise;
1575
- } catch (error) {
1576
- this.inflightConnectionPromises.delete(to);
1577
- const errStr = coerceErrorString(error);
1578
- if (!this.reconnectOnConnectionDrop || !canProceedWithConnection()) {
1579
- this.log?.warn(`connection to ${to} failed (${errStr})`, {
1580
- clientId: this.clientId,
1581
- connectedTo: to
1582
- });
1583
- } else {
1584
- this.log?.warn(`connection to ${to} failed (${errStr}), retrying`, {
1585
- clientId: this.clientId,
1586
- connectedTo: to
1587
- });
1588
- await this.connect(to);
1589
- }
1585
+ let sleep = Promise.resolve();
1586
+ const backoffMs = this.retryBudget.getBackoffMs(to);
1587
+ if (backoffMs > 0) {
1588
+ sleep = new Promise((resolve) => setTimeout(resolve, backoffMs));
1590
1589
  }
1591
- }
1592
- deleteSession({
1593
- session,
1594
- closeHandshakingConnection,
1595
- handshakingConn
1596
- }) {
1597
- this.inflightConnectionPromises.delete(session.to);
1598
- super.deleteSession({
1599
- session,
1600
- closeHandshakingConnection,
1601
- handshakingConn
1590
+ this.log?.info(
1591
+ `attempting connection to ${to} (${backoffMs}ms backoff)`,
1592
+ session.loggingMetadata
1593
+ );
1594
+ this.retryBudget.consumeBudget(to);
1595
+ const reconnectPromise = tracing_default.startActiveSpan("connect", async (span) => {
1596
+ try {
1597
+ span.addEvent("backoff", { backoffMs });
1598
+ await sleep;
1599
+ if (this.getStatus() !== "open") {
1600
+ throw new Error("transport state is no longer open");
1601
+ }
1602
+ span.addEvent("connecting");
1603
+ return await this.createNewOutgoingConnection(to);
1604
+ } catch (err) {
1605
+ const errStr = coerceErrorString(err);
1606
+ span.recordException(errStr);
1607
+ span.setStatus({ code: import_api3.SpanStatusCode.ERROR });
1608
+ throw err;
1609
+ } finally {
1610
+ span.end();
1611
+ }
1602
1612
  });
1613
+ const connectingSession = SessionStateGraph.transition.NoConnectionToConnecting(
1614
+ session,
1615
+ reconnectPromise,
1616
+ {
1617
+ onConnectionEstablished: (conn) => {
1618
+ this.log?.debug(
1619
+ `connection to ${connectingSession.to} established`,
1620
+ connectingSession.loggingMetadata
1621
+ );
1622
+ this.onConnectionEstablished(connectingSession, conn);
1623
+ },
1624
+ onConnectionFailed: (error) => {
1625
+ const errStr = coerceErrorString(error);
1626
+ this.log?.error(
1627
+ `error connecting to ${connectingSession.to}: ${errStr}`,
1628
+ connectingSession.loggingMetadata
1629
+ );
1630
+ this.onConnectingFailed(connectingSession);
1631
+ },
1632
+ onConnectionTimeout: () => {
1633
+ this.log?.error(
1634
+ `connection to ${connectingSession.to} timed out`,
1635
+ connectingSession.loggingMetadata
1636
+ );
1637
+ this.onConnectingFailed(connectingSession);
1638
+ }
1639
+ }
1640
+ );
1641
+ this.updateSession(connectingSession);
1603
1642
  }
1604
- async sendHandshake(to, conn) {
1643
+ async sendHandshake(session) {
1605
1644
  let metadata = void 0;
1606
1645
  if (this.handshakeExtensions) {
1607
1646
  metadata = await this.handshakeExtensions.construct();
1608
- if (!import_value2.Value.Check(this.handshakeExtensions.schema, metadata)) {
1609
- this.log?.error(`constructed handshake metadata did not match schema`, {
1610
- ...conn.loggingMetadata,
1611
- clientId: this.clientId,
1612
- connectedTo: to,
1613
- validationErrors: [
1614
- ...import_value2.Value.Errors(this.handshakeExtensions.schema, metadata)
1615
- ],
1616
- tags: ["invariant-violation"]
1617
- });
1618
- this.protocolError(
1619
- ProtocolError.HandshakeFailed,
1620
- "handshake metadata did not match schema"
1621
- );
1622
- conn.telemetry?.span.setStatus({
1623
- code: import_api4.SpanStatusCode.ERROR,
1624
- message: "handshake meta mismatch"
1625
- });
1626
- return false;
1627
- }
1628
1647
  }
1629
- const { session } = this.getOrCreateSession({ to, handshakingConn: conn });
1630
1648
  const requestMsg = handshakeRequestMessage({
1631
1649
  from: this.clientId,
1632
- to,
1650
+ to: session.to,
1633
1651
  sessionId: session.id,
1634
1652
  expectedSessionState: {
1635
- reconnect: session.advertisedSessionId !== void 0,
1636
- nextExpectedSeq: session.nextExpectedSeq
1653
+ nextExpectedSeq: session.ack,
1654
+ nextSentSeq: session.nextSeq()
1637
1655
  },
1638
1656
  metadata,
1639
1657
  tracing: getPropagationContext(session.telemetry.ctx)
1640
1658
  });
1641
- this.log?.debug(`sending handshake request to ${to}`, {
1642
- ...conn.loggingMetadata,
1643
- clientId: this.clientId,
1644
- connectedTo: to,
1659
+ this.log?.debug(`sending handshake request to ${session.to}`, {
1660
+ ...session.loggingMetadata,
1645
1661
  transportMessage: requestMsg
1646
1662
  });
1647
- conn.send(this.codec.toBuffer(requestMsg));
1648
- return true;
1663
+ session.sendHandshake(requestMsg);
1649
1664
  }
1650
1665
  close() {
1651
1666
  this.retryBudget.close();
@@ -1661,10 +1676,6 @@ var UnixDomainSocketClientTransport = class extends ClientTransport {
1661
1676
  this.path = socketPath;
1662
1677
  }
1663
1678
  async createNewOutgoingConnection(to) {
1664
- const oldConnection = this.connections.get(to);
1665
- if (oldConnection) {
1666
- oldConnection.close();
1667
- }
1668
1679
  this.log?.info(`establishing a new uds to ${to}`, {
1669
1680
  clientId: this.clientId,
1670
1681
  connectedTo: to
@@ -1675,9 +1686,7 @@ var UnixDomainSocketClientTransport = class extends ClientTransport {
1675
1686
  sock2.on("error", (err) => reject(err));
1676
1687
  sock2.connect(this.path);
1677
1688
  });
1678
- const conn = new UdsConnection(sock);
1679
- this.handleConnection(conn, to);
1680
- return conn;
1689
+ return new UdsConnection(sock);
1681
1690
  }
1682
1691
  };
1683
1692
  // Annotate the CommonJS export names for ESM import in node: