@replit/river 0.23.18 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +17 -16
  2. package/dist/{chunk-AVL32IMG.js → chunk-AASMR3CQ.js} +20 -16
  3. package/dist/chunk-AASMR3CQ.js.map +1 -0
  4. package/dist/chunk-JA57I7MG.js +653 -0
  5. package/dist/chunk-JA57I7MG.js.map +1 -0
  6. package/dist/chunk-KX5PQRVN.js +382 -0
  7. package/dist/chunk-KX5PQRVN.js.map +1 -0
  8. package/dist/{chunk-EV5HW4IC.js → chunk-KYYB4DUR.js} +65 -53
  9. package/dist/chunk-KYYB4DUR.js.map +1 -0
  10. package/dist/chunk-NLQPPDOT.js +399 -0
  11. package/dist/chunk-NLQPPDOT.js.map +1 -0
  12. package/dist/{chunk-R2HAS3GM.js → chunk-PJGGC3LV.js} +55 -41
  13. package/dist/chunk-PJGGC3LV.js.map +1 -0
  14. package/dist/{chunk-7MJYOL32.js → chunk-RXJLI2OP.js} +15 -23
  15. package/dist/chunk-RXJLI2OP.js.map +1 -0
  16. package/dist/{chunk-6LCL2ZZF.js → chunk-TAH2GVTJ.js} +1 -1
  17. package/dist/chunk-TAH2GVTJ.js.map +1 -0
  18. package/dist/chunk-ZAT3R4CU.js +277 -0
  19. package/dist/chunk-ZAT3R4CU.js.map +1 -0
  20. package/dist/{client-5776a6bb.d.ts → client-ba0d3315.d.ts} +12 -15
  21. package/dist/{connection-bd35d442.d.ts → connection-c3a96d09.d.ts} +1 -5
  22. package/dist/connection-d33e3246.d.ts +11 -0
  23. package/dist/{handshake-a947c234.d.ts → handshake-cdead82a.d.ts} +148 -183
  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-ea74cdbb.d.ts → message-e6c560fd.d.ts} +2 -2
  29. package/dist/router/index.cjs +104 -63
  30. package/dist/router/index.cjs.map +1 -1
  31. package/dist/router/index.d.cts +11 -10
  32. package/dist/router/index.d.ts +11 -10
  33. package/dist/router/index.js +2 -2
  34. package/dist/server-2ef5e6ec.d.ts +42 -0
  35. package/dist/{services-38b3f758.d.ts → services-e1417b33.d.ts} +3 -3
  36. package/dist/transport/impls/uds/client.cjs +1246 -1230
  37. package/dist/transport/impls/uds/client.cjs.map +1 -1
  38. package/dist/transport/impls/uds/client.d.cts +4 -4
  39. package/dist/transport/impls/uds/client.d.ts +4 -4
  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 +1298 -1151
  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 +976 -965
  48. package/dist/transport/impls/ws/client.cjs.map +1 -1
  49. package/dist/transport/impls/ws/client.d.cts +4 -4
  50. package/dist/transport/impls/ws/client.d.ts +4 -4
  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 +1182 -1047
  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 +1433 -1360
  59. package/dist/transport/index.cjs.map +1 -1
  60. package/dist/transport/index.d.cts +4 -4
  61. package/dist/transport/index.d.ts +4 -4
  62. package/dist/transport/index.js +9 -9
  63. package/dist/util/testHelpers.cjs +743 -310
  64. package/dist/util/testHelpers.cjs.map +1 -1
  65. package/dist/util/testHelpers.d.cts +9 -6
  66. package/dist/util/testHelpers.d.ts +9 -6
  67. package/dist/util/testHelpers.js +33 -10
  68. package/dist/util/testHelpers.js.map +1 -1
  69. package/package.json +1 -1
  70. package/dist/chunk-6LCL2ZZF.js.map +0 -1
  71. package/dist/chunk-7MJYOL32.js.map +0 -1
  72. package/dist/chunk-AVL32IMG.js.map +0 -1
  73. package/dist/chunk-DPKOJQWF.js +0 -476
  74. package/dist/chunk-DPKOJQWF.js.map +0 -1
  75. package/dist/chunk-EV5HW4IC.js.map +0 -1
  76. package/dist/chunk-J6N6H2WU.js +0 -476
  77. package/dist/chunk-J6N6H2WU.js.map +0 -1
  78. package/dist/chunk-MW5JXLHY.js +0 -348
  79. package/dist/chunk-MW5JXLHY.js.map +0 -1
  80. package/dist/chunk-R2HAS3GM.js.map +0 -1
  81. package/dist/chunk-RJOWZIWB.js +0 -335
  82. package/dist/chunk-RJOWZIWB.js.map +0 -1
  83. package/dist/connection-df85db7e.d.ts +0 -17
  84. package/dist/server-53cd5b7e.d.ts +0 -24
@@ -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(),
@@ -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,12 +303,12 @@ 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: {
@@ -125,521 +320,58 @@ function handshakeRequestMessage({
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.23.18";
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 || this.sock.closed) {
581
- return false;
582
- }
583
- this.sock.write(MessageFramer.write(payload));
584
- return true;
585
- }
586
- close() {
587
- this.sock.destroy();
588
- this.framer.destroy();
589
- }
590
- };
591
-
592
- // transport/client.ts
593
- var import_api4 = require("@opentelemetry/api");
594
-
595
- // codec/json.ts
596
- var encoder = new TextEncoder();
597
- var decoder = new TextDecoder();
598
- function uint8ArrayToBase64(uint8Array) {
599
- let binary = "";
600
- uint8Array.forEach((byte) => {
601
- binary += String.fromCharCode(byte);
602
- });
603
- return btoa(binary);
604
- }
605
- function base64ToUint8Array(base64) {
606
- const binaryString = atob(base64);
607
- const uint8Array = new Uint8Array(binaryString.length);
608
- for (let i = 0; i < binaryString.length; i++) {
609
- uint8Array[i] = binaryString.charCodeAt(i);
610
- }
611
- return uint8Array;
612
- }
613
- var NaiveJsonCodec = {
614
- toBuffer: (obj) => {
615
- return encoder.encode(
616
- JSON.stringify(obj, function replacer(key) {
617
- const val = this[key];
618
- if (val instanceof Uint8Array) {
619
- return { $t: uint8ArrayToBase64(val) };
620
- } else {
621
- return val;
622
- }
623
- })
624
- );
625
- },
626
- fromBuffer: (buff) => {
627
- try {
628
- const parsed = JSON.parse(
629
- decoder.decode(buff),
630
- function reviver(_key, val) {
631
- if (val?.$t) {
632
- return base64ToUint8Array(val.$t);
633
- } else {
634
- return val;
635
- }
636
- }
637
- );
638
- if (typeof parsed === "object")
639
- return parsed;
640
- return null;
641
- } catch {
642
- 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;
643
375
  }
644
376
  }
645
377
  };
@@ -649,7 +381,8 @@ var defaultTransportOptions = {
649
381
  heartbeatIntervalMs: 1e3,
650
382
  heartbeatsUntilDead: 2,
651
383
  sessionDisconnectGraceMs: 5e3,
652
- handshakeTimeoutMs: 5e3,
384
+ connectionTimeoutMs: 2e3,
385
+ handshakeTimeoutMs: 1e3,
653
386
  codec: NaiveJsonCodec
654
387
  };
655
388
  var defaultConnectionRetryOptions = {
@@ -738,9 +471,6 @@ var LeakyBucketRateLimit = class {
738
471
  }
739
472
  };
740
473
 
741
- // transport/transport.ts
742
- var import_value = require("@sinclair/typebox/value");
743
-
744
474
  // logging/log.ts
745
475
  var LoggingLevels = {
746
476
  debug: -1,
@@ -766,98 +496,719 @@ var BaseLogger = class {
766
496
  this.minLevel = minLevel;
767
497
  this.output = output;
768
498
  }
769
- debug(msg, metadata) {
770
- if (LoggingLevels[this.minLevel] <= LoggingLevels.debug) {
771
- this.output(msg, metadata ?? {}, "debug");
772
- }
499
+ debug(msg, metadata) {
500
+ if (LoggingLevels[this.minLevel] <= LoggingLevels.debug) {
501
+ this.output(msg, metadata ?? {}, "debug");
502
+ }
503
+ }
504
+ info(msg, metadata) {
505
+ if (LoggingLevels[this.minLevel] <= LoggingLevels.info) {
506
+ this.output(msg, metadata ?? {}, "info");
507
+ }
508
+ }
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
+ /**
650
+ * Index of the message we will send next (excluding handshake)
651
+ */
652
+ seq;
653
+ /**
654
+ * Number of unique messages we've received this session (excluding handshake)
655
+ */
656
+ ack;
657
+ sendBuffer;
658
+ constructor(id, from, to, seq, ack, sendBuffer, telemetry, options, log) {
659
+ super(from, options, log);
660
+ this.id = id;
661
+ this.to = to;
662
+ this.seq = seq;
663
+ this.ack = ack;
664
+ this.sendBuffer = sendBuffer;
665
+ this.telemetry = telemetry;
666
+ this.log = log;
667
+ }
668
+ get loggingMetadata() {
669
+ const spanContext = this.telemetry.span.spanContext();
670
+ return {
671
+ clientId: this.from,
672
+ connectedTo: this.to,
673
+ sessionId: this.id,
674
+ telemetry: {
675
+ traceId: spanContext.traceId,
676
+ spanId: spanContext.spanId
677
+ }
678
+ };
679
+ }
680
+ constructMsg(partialMsg) {
681
+ const msg = {
682
+ ...partialMsg,
683
+ id: generateId(),
684
+ to: this.to,
685
+ from: this.from,
686
+ seq: this.seq,
687
+ ack: this.ack
688
+ };
689
+ this.seq++;
690
+ return msg;
691
+ }
692
+ nextSeq() {
693
+ return this.sendBuffer.length > 0 ? this.sendBuffer[0].seq : this.seq;
694
+ }
695
+ send(msg) {
696
+ const constructedMsg = this.constructMsg(msg);
697
+ this.sendBuffer.push(constructedMsg);
698
+ return constructedMsg.id;
699
+ }
700
+ _handleStateExit() {
701
+ }
702
+ _handleClose() {
703
+ this.sendBuffer.length = 0;
704
+ this.telemetry.span.end();
705
+ }
706
+ };
707
+
708
+ // transport/sessionStateMachine/SessionConnecting.ts
709
+ var SessionConnecting = class extends IdentifiedSession {
710
+ state = "Connecting" /* Connecting */;
711
+ connPromise;
712
+ listeners;
713
+ connectionTimeout;
714
+ constructor(connPromise, listeners, ...args) {
715
+ super(...args);
716
+ this.connPromise = connPromise;
717
+ this.listeners = listeners;
718
+ this.connectionTimeout = setTimeout(() => {
719
+ listeners.onConnectionTimeout();
720
+ }, this.options.connectionTimeoutMs);
721
+ connPromise.then(
722
+ (conn) => {
723
+ if (this._isConsumed)
724
+ return;
725
+ listeners.onConnectionEstablished(conn);
726
+ },
727
+ (err) => {
728
+ if (this._isConsumed)
729
+ return;
730
+ listeners.onConnectionFailed(err);
731
+ }
732
+ );
733
+ }
734
+ // close a pending connection if it resolves, ignore errors if the promise
735
+ // ends up rejected anyways
736
+ bestEffortClose() {
737
+ void this.connPromise.then((conn) => conn.close()).catch(() => {
738
+ });
739
+ }
740
+ _handleStateExit() {
741
+ super._handleStateExit();
742
+ clearTimeout(this.connectionTimeout);
743
+ this.connectionTimeout = void 0;
744
+ }
745
+ _handleClose() {
746
+ this.bestEffortClose();
747
+ super._handleClose();
748
+ }
749
+ };
750
+
751
+ // transport/sessionStateMachine/SessionNoConnection.ts
752
+ var SessionNoConnection = class extends IdentifiedSession {
753
+ state = "NoConnection" /* NoConnection */;
754
+ listeners;
755
+ gracePeriodTimeout;
756
+ constructor(listeners, ...args) {
757
+ super(...args);
758
+ this.listeners = listeners;
759
+ this.gracePeriodTimeout = setTimeout(() => {
760
+ this.listeners.onSessionGracePeriodElapsed();
761
+ }, this.options.sessionDisconnectGraceMs);
762
+ }
763
+ _handleClose() {
764
+ super._handleClose();
765
+ }
766
+ _handleStateExit() {
767
+ super._handleStateExit();
768
+ if (this.gracePeriodTimeout) {
769
+ clearTimeout(this.gracePeriodTimeout);
770
+ this.gracePeriodTimeout = void 0;
771
+ }
772
+ }
773
+ };
774
+
775
+ // tracing/index.ts
776
+ var import_api = require("@opentelemetry/api");
777
+
778
+ // package.json
779
+ var version = "0.24.0";
780
+
781
+ // tracing/index.ts
782
+ function getPropagationContext(ctx) {
783
+ const tracing = {
784
+ traceparent: "",
785
+ tracestate: ""
786
+ };
787
+ import_api.propagation.inject(ctx, tracing);
788
+ return tracing;
789
+ }
790
+ function createSessionTelemetryInfo(sessionId, to, from, propagationCtx) {
791
+ const parentCtx = propagationCtx ? import_api.propagation.extract(import_api.context.active(), propagationCtx) : import_api.context.active();
792
+ const span = tracer.startSpan(
793
+ `session ${sessionId}`,
794
+ {
795
+ attributes: {
796
+ component: "river",
797
+ "river.session.id": sessionId,
798
+ "river.session.to": to,
799
+ "river.session.from": from
800
+ }
801
+ },
802
+ parentCtx
803
+ );
804
+ const ctx = import_api.trace.setSpan(parentCtx, span);
805
+ return { span, ctx };
806
+ }
807
+ var tracer = import_api.trace.getTracer("river", version);
808
+ var tracing_default = tracer;
809
+
810
+ // transport/sessionStateMachine/SessionWaitingForHandshake.ts
811
+ var SessionWaitingForHandshake = class extends CommonSession {
812
+ state = "WaitingForHandshake" /* WaitingForHandshake */;
813
+ conn;
814
+ listeners;
815
+ handshakeTimeout;
816
+ constructor(conn, listeners, ...args) {
817
+ super(...args);
818
+ this.conn = conn;
819
+ this.listeners = listeners;
820
+ this.handshakeTimeout = setTimeout(() => {
821
+ listeners.onHandshakeTimeout();
822
+ }, this.options.handshakeTimeoutMs);
823
+ this.conn.addDataListener(this.onHandshakeData);
824
+ this.conn.addErrorListener(listeners.onConnectionErrored);
825
+ this.conn.addCloseListener(listeners.onConnectionClosed);
826
+ }
827
+ onHandshakeData = (msg) => {
828
+ const parsedMsg = this.parseMsg(msg);
829
+ if (parsedMsg === null) {
830
+ this.listeners.onInvalidHandshake("could not parse message");
831
+ return;
832
+ }
833
+ this.listeners.onHandshake(parsedMsg);
834
+ };
835
+ get loggingMetadata() {
836
+ return {
837
+ clientId: this.from,
838
+ connId: this.conn.id
839
+ };
840
+ }
841
+ sendHandshake(msg) {
842
+ return this.conn.send(this.options.codec.toBuffer(msg));
773
843
  }
774
- info(msg, metadata) {
775
- if (LoggingLevels[this.minLevel] <= LoggingLevels.info) {
776
- this.output(msg, metadata ?? {}, "info");
777
- }
844
+ _handleStateExit() {
845
+ this.conn.removeDataListener(this.onHandshakeData);
846
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
847
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
848
+ clearTimeout(this.handshakeTimeout);
849
+ this.handshakeTimeout = void 0;
778
850
  }
779
- warn(msg, metadata) {
780
- if (LoggingLevels[this.minLevel] <= LoggingLevels.warn) {
781
- this.output(msg, metadata ?? {}, "warn");
782
- }
851
+ _handleClose() {
852
+ this.conn.close();
783
853
  }
784
- error(msg, metadata) {
785
- if (LoggingLevels[this.minLevel] <= LoggingLevels.error) {
786
- this.output(msg, metadata ?? {}, "error");
854
+ };
855
+
856
+ // transport/sessionStateMachine/SessionHandshaking.ts
857
+ var SessionHandshaking = class extends IdentifiedSession {
858
+ state = "Handshaking" /* Handshaking */;
859
+ conn;
860
+ listeners;
861
+ handshakeTimeout;
862
+ constructor(conn, listeners, ...args) {
863
+ super(...args);
864
+ this.conn = conn;
865
+ this.listeners = listeners;
866
+ this.handshakeTimeout = setTimeout(() => {
867
+ listeners.onHandshakeTimeout();
868
+ }, this.options.handshakeTimeoutMs);
869
+ this.conn.addDataListener(this.onHandshakeData);
870
+ this.conn.addErrorListener(listeners.onConnectionErrored);
871
+ this.conn.addCloseListener(listeners.onConnectionClosed);
872
+ }
873
+ onHandshakeData = (msg) => {
874
+ const parsedMsg = this.parseMsg(msg);
875
+ if (parsedMsg === null) {
876
+ this.listeners.onInvalidHandshake("could not parse message");
877
+ return;
787
878
  }
879
+ this.listeners.onHandshake(parsedMsg);
880
+ };
881
+ sendHandshake(msg) {
882
+ return this.conn.send(this.options.codec.toBuffer(msg));
883
+ }
884
+ _handleStateExit() {
885
+ super._handleStateExit();
886
+ this.conn.removeDataListener(this.onHandshakeData);
887
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
888
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
889
+ clearTimeout(this.handshakeTimeout);
890
+ }
891
+ _handleClose() {
892
+ super._handleClose();
893
+ this.conn.close();
788
894
  }
789
895
  };
790
- var createLogProxy = (log) => ({
791
- debug: cleanedLogFn(log.debug.bind(log)),
792
- info: cleanedLogFn(log.info.bind(log)),
793
- warn: cleanedLogFn(log.warn.bind(log)),
794
- error: cleanedLogFn(log.error.bind(log))
795
- });
796
896
 
797
- // transport/events.ts
798
- var ProtocolError = {
799
- RetriesExceeded: "conn_retry_exceeded",
800
- HandshakeFailed: "handshake_failed",
801
- MessageOrderingViolated: "message_ordering_violated"
802
- };
803
- var EventDispatcher = class {
804
- eventListeners = {};
805
- removeAllListeners() {
806
- this.eventListeners = {};
897
+ // transport/sessionStateMachine/SessionConnected.ts
898
+ var import_api2 = require("@opentelemetry/api");
899
+ var SessionConnected = class extends IdentifiedSession {
900
+ state = "Connected" /* Connected */;
901
+ conn;
902
+ listeners;
903
+ heartbeatHandle;
904
+ heartbeatMisses = 0;
905
+ get isActivelyHeartbeating() {
906
+ return this.heartbeatHandle !== void 0;
807
907
  }
808
- numberOfListeners(eventType) {
809
- return this.eventListeners[eventType]?.size ?? 0;
908
+ updateBookkeeping(ack, seq) {
909
+ this.sendBuffer = this.sendBuffer.filter((unacked) => unacked.seq >= ack);
910
+ this.ack = seq + 1;
911
+ this.heartbeatMisses = 0;
810
912
  }
811
- addEventListener(eventType, handler) {
812
- if (!this.eventListeners[eventType]) {
813
- this.eventListeners[eventType] = /* @__PURE__ */ new Set();
913
+ send(msg) {
914
+ const constructedMsg = this.constructMsg(msg);
915
+ this.sendBuffer.push(constructedMsg);
916
+ this.conn.send(this.options.codec.toBuffer(constructedMsg));
917
+ return constructedMsg.id;
918
+ }
919
+ constructor(conn, listeners, ...args) {
920
+ super(...args);
921
+ this.conn = conn;
922
+ this.listeners = listeners;
923
+ this.conn.addDataListener(this.onMessageData);
924
+ this.conn.addCloseListener(listeners.onConnectionClosed);
925
+ this.conn.addErrorListener(listeners.onConnectionErrored);
926
+ if (this.sendBuffer.length > 0) {
927
+ this.log?.debug(
928
+ `sending ${this.sendBuffer.length} buffered messages`,
929
+ this.loggingMetadata
930
+ );
814
931
  }
815
- this.eventListeners[eventType]?.add(handler);
816
- }
817
- removeEventListener(eventType, handler) {
818
- const handlers = this.eventListeners[eventType];
819
- if (handlers) {
820
- this.eventListeners[eventType]?.delete(handler);
932
+ for (const msg of this.sendBuffer) {
933
+ conn.send(this.options.codec.toBuffer(msg));
821
934
  }
822
935
  }
823
- dispatchEvent(eventType, event) {
824
- const handlers = this.eventListeners[eventType];
825
- if (handlers) {
826
- const copy = [...handlers];
827
- for (const handler of copy) {
828
- handler(event);
936
+ startActiveHeartbeat() {
937
+ this.heartbeatHandle = setInterval(() => {
938
+ const misses = this.heartbeatMisses;
939
+ const missDuration = misses * this.options.heartbeatIntervalMs;
940
+ if (misses >= this.options.heartbeatsUntilDead) {
941
+ this.log?.info(
942
+ `closing connection to ${this.to} due to inactivity (missed ${misses} heartbeats which is ${missDuration}ms)`,
943
+ this.loggingMetadata
944
+ );
945
+ this.telemetry.span.addEvent("closing connection due to inactivity");
946
+ this.conn.close();
947
+ clearInterval(this.heartbeatHandle);
948
+ this.heartbeatHandle = void 0;
949
+ return;
950
+ }
951
+ this.sendHeartbeat();
952
+ this.heartbeatMisses++;
953
+ }, this.options.heartbeatIntervalMs);
954
+ }
955
+ sendHeartbeat() {
956
+ this.send({
957
+ streamId: "heartbeat",
958
+ controlFlags: 1 /* AckBit */,
959
+ payload: {
960
+ type: "ACK"
961
+ }
962
+ });
963
+ }
964
+ onMessageData = (msg) => {
965
+ const parsedMsg = this.parseMsg(msg);
966
+ if (parsedMsg === null)
967
+ return;
968
+ if (parsedMsg.seq !== this.ack) {
969
+ if (parsedMsg.seq < this.ack) {
970
+ this.log?.debug(
971
+ `received duplicate msg (got seq: ${parsedMsg.seq}, wanted seq: ${this.ack}), discarding`,
972
+ {
973
+ ...this.loggingMetadata,
974
+ transportMessage: parsedMsg
975
+ }
976
+ );
977
+ } else {
978
+ const reason = `received out-of-order msg (got seq: ${parsedMsg.seq}, wanted seq: ${this.ack})`;
979
+ this.log?.error(reason, {
980
+ ...this.loggingMetadata,
981
+ transportMessage: parsedMsg,
982
+ tags: ["invariant-violation"]
983
+ });
984
+ this.telemetry.span.setStatus({
985
+ code: import_api2.SpanStatusCode.ERROR,
986
+ message: reason
987
+ });
988
+ this.listeners.onInvalidMessage(reason);
829
989
  }
990
+ return;
991
+ }
992
+ this.log?.debug(`received msg`, {
993
+ ...this.loggingMetadata,
994
+ transportMessage: parsedMsg
995
+ });
996
+ this.updateBookkeeping(parsedMsg.ack, parsedMsg.seq);
997
+ if (!isAck(parsedMsg.controlFlags)) {
998
+ this.listeners.onMessage(parsedMsg);
999
+ return;
1000
+ }
1001
+ this.log?.debug(`discarding msg (ack bit set)`, {
1002
+ ...this.loggingMetadata,
1003
+ transportMessage: parsedMsg
1004
+ });
1005
+ if (!this.isActivelyHeartbeating) {
1006
+ this.sendHeartbeat();
1007
+ }
1008
+ };
1009
+ _handleStateExit() {
1010
+ super._handleStateExit();
1011
+ this.conn.removeDataListener(this.onMessageData);
1012
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
1013
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
1014
+ clearInterval(this.heartbeatHandle);
1015
+ this.heartbeatHandle = void 0;
1016
+ }
1017
+ _handleClose() {
1018
+ super._handleClose();
1019
+ this.conn.close();
1020
+ }
1021
+ };
1022
+
1023
+ // transport/sessionStateMachine/transitions.ts
1024
+ function inheritSharedSession(session) {
1025
+ return [
1026
+ session.id,
1027
+ session.from,
1028
+ session.to,
1029
+ session.seq,
1030
+ session.ack,
1031
+ session.sendBuffer,
1032
+ session.telemetry,
1033
+ session.options,
1034
+ session.log
1035
+ ];
1036
+ }
1037
+ var SessionStateGraph = {
1038
+ entrypoints: {
1039
+ NoConnection(to, from, listeners, options, log) {
1040
+ const id = `session-${generateId()}`;
1041
+ const telemetry = createSessionTelemetryInfo(id, to, from);
1042
+ const sendBuffer = [];
1043
+ const session = new SessionNoConnection(
1044
+ listeners,
1045
+ id,
1046
+ from,
1047
+ to,
1048
+ 0,
1049
+ 0,
1050
+ sendBuffer,
1051
+ telemetry,
1052
+ options,
1053
+ log
1054
+ );
1055
+ session.log?.info(`session ${session.id} created in NoConnection state`, {
1056
+ ...session.loggingMetadata,
1057
+ tags: ["state-transition"]
1058
+ });
1059
+ return session;
1060
+ },
1061
+ WaitingForHandshake(from, conn, listeners, options, log) {
1062
+ const session = new SessionWaitingForHandshake(
1063
+ conn,
1064
+ listeners,
1065
+ from,
1066
+ options,
1067
+ log
1068
+ );
1069
+ session.log?.info(`session created in WaitingForHandshake state`, {
1070
+ ...session.loggingMetadata,
1071
+ tags: ["state-transition"]
1072
+ });
1073
+ return session;
1074
+ }
1075
+ },
1076
+ // All of the transitions 'move'/'consume' the old session and return a new one.
1077
+ // After a session is transitioned, any usage of the old session will throw.
1078
+ transition: {
1079
+ // happy path transitions
1080
+ NoConnectionToConnecting(oldSession, connPromise, listeners) {
1081
+ const carriedState = inheritSharedSession(oldSession);
1082
+ oldSession._handleStateExit();
1083
+ const session = new SessionConnecting(
1084
+ connPromise,
1085
+ listeners,
1086
+ ...carriedState
1087
+ );
1088
+ session.log?.info(
1089
+ `session ${session.id} transition from NoConnection to Connecting`,
1090
+ {
1091
+ ...session.loggingMetadata,
1092
+ tags: ["state-transition"]
1093
+ }
1094
+ );
1095
+ return session;
1096
+ },
1097
+ ConnectingToHandshaking(oldSession, conn, listeners) {
1098
+ const carriedState = inheritSharedSession(oldSession);
1099
+ oldSession._handleStateExit();
1100
+ const session = new SessionHandshaking(conn, listeners, ...carriedState);
1101
+ session.log?.info(
1102
+ `session ${session.id} transition from Connecting to Handshaking`,
1103
+ {
1104
+ ...session.loggingMetadata,
1105
+ tags: ["state-transition"]
1106
+ }
1107
+ );
1108
+ return session;
1109
+ },
1110
+ HandshakingToConnected(oldSession, listeners) {
1111
+ const carriedState = inheritSharedSession(oldSession);
1112
+ const conn = oldSession.conn;
1113
+ oldSession._handleStateExit();
1114
+ const session = new SessionConnected(conn, listeners, ...carriedState);
1115
+ session.log?.info(
1116
+ `session ${session.id} transition from Handshaking to Connected`,
1117
+ {
1118
+ ...session.loggingMetadata,
1119
+ tags: ["state-transition"]
1120
+ }
1121
+ );
1122
+ return session;
1123
+ },
1124
+ WaitingForHandshakeToConnected(pendingSession, oldSession, sessionId, to, propagationCtx, listeners) {
1125
+ const conn = pendingSession.conn;
1126
+ const { from, options } = pendingSession;
1127
+ const carriedState = oldSession ? (
1128
+ // old session exists, inherit state
1129
+ inheritSharedSession(oldSession)
1130
+ ) : (
1131
+ // old session does not exist, create new state
1132
+ [
1133
+ sessionId,
1134
+ from,
1135
+ to,
1136
+ 0,
1137
+ 0,
1138
+ [],
1139
+ createSessionTelemetryInfo(sessionId, to, from, propagationCtx),
1140
+ options,
1141
+ pendingSession.log
1142
+ ]
1143
+ );
1144
+ pendingSession._handleStateExit();
1145
+ oldSession?._handleStateExit();
1146
+ const session = new SessionConnected(conn, listeners, ...carriedState);
1147
+ session.log?.info(
1148
+ `session ${session.id} transition from WaitingForHandshake to Connected`,
1149
+ {
1150
+ ...session.loggingMetadata,
1151
+ tags: ["state-transition"]
1152
+ }
1153
+ );
1154
+ return session;
1155
+ },
1156
+ // disconnect paths
1157
+ ConnectingToNoConnection(oldSession, listeners) {
1158
+ const carriedState = inheritSharedSession(oldSession);
1159
+ oldSession.bestEffortClose();
1160
+ oldSession._handleStateExit();
1161
+ const session = new SessionNoConnection(listeners, ...carriedState);
1162
+ session.log?.info(
1163
+ `session ${session.id} transition from Connecting to NoConnection`,
1164
+ {
1165
+ ...session.loggingMetadata,
1166
+ tags: ["state-transition"]
1167
+ }
1168
+ );
1169
+ return session;
1170
+ },
1171
+ HandshakingToNoConnection(oldSession, listeners) {
1172
+ const carriedState = inheritSharedSession(oldSession);
1173
+ oldSession.conn.close();
1174
+ oldSession._handleStateExit();
1175
+ const session = new SessionNoConnection(listeners, ...carriedState);
1176
+ session.log?.info(
1177
+ `session ${session.id} transition from Handshaking to NoConnection`,
1178
+ {
1179
+ ...session.loggingMetadata,
1180
+ tags: ["state-transition"]
1181
+ }
1182
+ );
1183
+ return session;
1184
+ },
1185
+ ConnectedToNoConnection(oldSession, listeners) {
1186
+ const carriedState = inheritSharedSession(oldSession);
1187
+ oldSession.conn.close();
1188
+ oldSession._handleStateExit();
1189
+ const session = new SessionNoConnection(listeners, ...carriedState);
1190
+ session.log?.info(
1191
+ `session ${session.id} transition from Connected to NoConnection`,
1192
+ {
1193
+ ...session.loggingMetadata,
1194
+ tags: ["state-transition"]
1195
+ }
1196
+ );
1197
+ return session;
830
1198
  }
831
1199
  }
832
1200
  };
833
1201
 
834
1202
  // transport/transport.ts
835
- var import_api3 = require("@opentelemetry/api");
836
1203
  var Transport = class {
837
1204
  /**
838
1205
  * The status of the transport.
839
1206
  */
840
1207
  status;
841
- /**
842
- * The {@link Codec} used to encode and decode messages.
843
- */
844
- codec;
845
1208
  /**
846
1209
  * The client ID of this transport.
847
1210
  */
848
1211
  clientId;
849
- /**
850
- * The map of {@link Session}s managed by this transport.
851
- */
852
- sessions;
853
- /**
854
- * The map of {@link Connection}s managed by this transport.
855
- */
856
- get connections() {
857
- return new Map(
858
- [...this.sessions].map(([client, session]) => [client, session.connection]).filter((entry) => entry[1] !== void 0)
859
- );
860
- }
861
1212
  /**
862
1213
  * The event dispatcher for handling events of type EventTypes.
863
1214
  */
@@ -867,320 +1218,35 @@ var Transport = class {
867
1218
  */
868
1219
  options;
869
1220
  log;
1221
+ sessions;
870
1222
  /**
871
- * Creates a new Transport instance.
872
- * This should also set up {@link onConnect}, and {@link onDisconnect} listeners.
873
- * @param codec The codec used to encode and decode messages.
874
- * @param clientId The client ID of this transport.
875
- */
876
- constructor(clientId, providedOptions) {
877
- this.options = { ...defaultTransportOptions, ...providedOptions };
878
- this.eventDispatcher = new EventDispatcher();
879
- this.sessions = /* @__PURE__ */ new Map();
880
- this.codec = this.options.codec;
881
- this.clientId = clientId;
882
- this.status = "open";
883
- }
884
- bindLogger(fn, level) {
885
- if (typeof fn === "function") {
886
- this.log = createLogProxy(new BaseLogger(fn, level));
887
- return;
888
- }
889
- this.log = createLogProxy(fn);
890
- }
891
- /**
892
- * Called when a new connection is established
893
- * and we know the identity of the connected client.
894
- * @param conn The connection object.
895
- */
896
- onConnect(conn, session, isTransparentReconnect) {
897
- this.eventDispatcher.dispatchEvent("connectionStatus", {
898
- status: "connect",
899
- conn
900
- });
901
- conn.telemetry = createConnectionTelemetryInfo(conn, session.telemetry);
902
- session.replaceWithNewConnection(conn, isTransparentReconnect);
903
- this.log?.info(`connected to ${session.to}`, {
904
- ...conn.loggingMetadata,
905
- ...session.loggingMetadata
906
- });
907
- }
908
- createSession(to, conn, propagationCtx) {
909
- const session = new Session(
910
- conn,
911
- this.clientId,
912
- to,
913
- this.options,
914
- propagationCtx
915
- );
916
- if (this.log) {
917
- session.bindLogger(this.log);
918
- }
919
- const currentSession = this.sessions.get(session.to);
920
- if (currentSession) {
921
- this.log?.warn(
922
- `session ${session.id} from ${session.to} surreptitiously replacing ${currentSession.id}`,
923
- {
924
- ...currentSession.loggingMetadata,
925
- tags: ["invariant-violation"]
926
- }
927
- );
928
- this.deleteSession({
929
- session: currentSession,
930
- closeHandshakingConnection: false
931
- });
932
- }
933
- this.sessions.set(session.to, session);
934
- this.eventDispatcher.dispatchEvent("sessionStatus", {
935
- status: "connect",
936
- session
937
- });
938
- return session;
939
- }
940
- createNewSession({
941
- to,
942
- conn,
943
- sessionId,
944
- propagationCtx
945
- }) {
946
- let session = this.sessions.get(to);
947
- if (session !== void 0) {
948
- this.log?.info(
949
- `session for ${to} already exists, replacing it with a new session as requested`,
950
- session.loggingMetadata
951
- );
952
- this.deleteSession({
953
- session,
954
- closeHandshakingConnection: false
955
- });
956
- session = void 0;
957
- }
958
- session = this.createSession(to, conn, propagationCtx);
959
- session.advertisedSessionId = sessionId;
960
- this.log?.info(`created new session for ${to}`, session.loggingMetadata);
961
- return session;
962
- }
963
- getExistingSession({
964
- to,
965
- sessionId,
966
- nextExpectedSeq
967
- }) {
968
- const session = this.sessions.get(to);
969
- if (
970
- // reject this request if there was no previous session to replace
971
- session === void 0 || // or if both parties do not agree about the next expected sequence number
972
- !session.nextExpectedSeqInRange(nextExpectedSeq) || // or if both parties do not agree on the advertised session id
973
- session.advertisedSessionId !== sessionId
974
- ) {
975
- return false;
976
- }
977
- this.log?.info(
978
- `reused existing session for ${to}`,
979
- session.loggingMetadata
980
- );
981
- return session;
982
- }
983
- getOrCreateSession({
984
- to,
985
- conn,
986
- handshakingConn,
987
- sessionId,
988
- propagationCtx
989
- }) {
990
- let session = this.sessions.get(to);
991
- const isReconnect = session !== void 0;
992
- let isTransparentReconnect = isReconnect;
993
- if (session?.advertisedSessionId !== void 0 && sessionId !== void 0 && session.advertisedSessionId !== sessionId) {
994
- this.log?.info(
995
- `session for ${to} already exists but has a different session id (expected: ${session.advertisedSessionId}, got: ${sessionId}), creating a new one`,
996
- session.loggingMetadata
997
- );
998
- this.deleteSession({
999
- session,
1000
- closeHandshakingConnection: handshakingConn !== void 0,
1001
- handshakingConn
1002
- });
1003
- isTransparentReconnect = false;
1004
- session = void 0;
1005
- }
1006
- if (!session) {
1007
- session = this.createSession(to, conn, propagationCtx);
1008
- this.log?.info(
1009
- `no session for ${to}, created a new one`,
1010
- session.loggingMetadata
1011
- );
1012
- }
1013
- if (sessionId !== void 0) {
1014
- session.advertisedSessionId = sessionId;
1015
- }
1016
- if (handshakingConn !== void 0) {
1017
- session.replaceWithNewHandshakingConnection(handshakingConn);
1018
- }
1019
- return { session, isReconnect, isTransparentReconnect };
1020
- }
1021
- deleteSession({
1022
- session,
1023
- closeHandshakingConnection,
1024
- handshakingConn
1025
- }) {
1026
- if (closeHandshakingConnection) {
1027
- session.closeHandshakingConnection(handshakingConn);
1028
- }
1029
- session.close();
1030
- session.telemetry.span.end();
1031
- const currentSession = this.sessions.get(session.to);
1032
- if (currentSession && currentSession.id !== session.id) {
1033
- this.log?.warn(
1034
- `session ${session.id} disconnect from ${session.to}, mismatch with ${currentSession.id}`,
1035
- {
1036
- ...session.loggingMetadata,
1037
- tags: ["invariant-violation"]
1038
- }
1039
- );
1040
- return;
1041
- }
1042
- this.sessions.delete(session.to);
1043
- this.log?.info(
1044
- `session ${session.id} disconnect from ${session.to}`,
1045
- session.loggingMetadata
1046
- );
1047
- this.eventDispatcher.dispatchEvent("sessionStatus", {
1048
- status: "disconnect",
1049
- session
1050
- });
1051
- }
1052
- /**
1053
- * The downstream implementation needs to call this when a connection is closed.
1054
- * @param conn The connection object.
1055
- * @param connectedTo The peer we are connected to.
1223
+ * Creates a new Transport instance.
1224
+ * @param codec The codec used to encode and decode messages.
1225
+ * @param clientId The client ID of this transport.
1056
1226
  */
1057
- onDisconnect(conn, session) {
1058
- if (session.connection !== void 0 && session.connection.id !== conn.id) {
1059
- session.telemetry.span.addEvent("onDisconnect race");
1060
- this.log?.warn("onDisconnect race", {
1061
- clientId: this.clientId,
1062
- ...session.loggingMetadata,
1063
- ...conn.loggingMetadata,
1064
- tags: ["invariant-violation"]
1065
- });
1066
- return;
1067
- }
1068
- conn.telemetry?.span.end();
1069
- this.eventDispatcher.dispatchEvent("connectionStatus", {
1070
- status: "disconnect",
1071
- conn
1072
- });
1073
- session.connection = void 0;
1074
- session.beginGrace(() => {
1075
- if (session.connection !== void 0) {
1076
- session.telemetry.span.addEvent("session grace period race");
1077
- this.log?.warn("session grace period race", {
1078
- clientId: this.clientId,
1079
- ...session.loggingMetadata,
1080
- ...conn.loggingMetadata,
1081
- tags: ["invariant-violation"]
1082
- });
1083
- return;
1084
- }
1085
- session.telemetry.span.addEvent("session grace period expired");
1086
- this.deleteSession({
1087
- session,
1088
- closeHandshakingConnection: true,
1089
- handshakingConn: conn
1090
- });
1091
- });
1227
+ constructor(clientId, providedOptions) {
1228
+ this.options = { ...defaultTransportOptions, ...providedOptions };
1229
+ this.eventDispatcher = new EventDispatcher();
1230
+ this.clientId = clientId;
1231
+ this.status = "open";
1232
+ this.sessions = /* @__PURE__ */ new Map();
1092
1233
  }
1093
- /**
1094
- * Parses a message from a Uint8Array into a {@link OpaqueTransportMessage}.
1095
- * @param msg The message to parse.
1096
- * @returns The parsed message, or null if the message is malformed or invalid.
1097
- */
1098
- parseMsg(msg, conn) {
1099
- const parsedMsg = this.codec.fromBuffer(msg);
1100
- if (parsedMsg === null) {
1101
- const decodedBuffer = new TextDecoder().decode(Buffer.from(msg));
1102
- this.log?.error(
1103
- `received malformed msg, killing conn: ${decodedBuffer}`,
1104
- {
1105
- clientId: this.clientId,
1106
- ...conn.loggingMetadata
1107
- }
1108
- );
1109
- return null;
1110
- }
1111
- if (!import_value.Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
1112
- this.log?.error(`received invalid msg: ${JSON.stringify(parsedMsg)}`, {
1113
- clientId: this.clientId,
1114
- ...conn.loggingMetadata,
1115
- validationErrors: [
1116
- ...import_value.Value.Errors(OpaqueTransportMessageSchema, parsedMsg)
1117
- ]
1118
- });
1119
- return null;
1234
+ bindLogger(fn, level) {
1235
+ if (typeof fn === "function") {
1236
+ this.log = createLogProxy(new BaseLogger(fn, level));
1237
+ return;
1120
1238
  }
1121
- return parsedMsg;
1239
+ this.log = createLogProxy(fn);
1122
1240
  }
1123
1241
  /**
1124
1242
  * Called when a message is received by this transport.
1125
1243
  * You generally shouldn't need to override this in downstream transport implementations.
1126
1244
  * @param msg The received message.
1127
1245
  */
1128
- handleMsg(msg, conn) {
1246
+ handleMsg(msg) {
1129
1247
  if (this.getStatus() !== "open")
1130
1248
  return;
1131
- const session = this.sessions.get(msg.from);
1132
- if (!session) {
1133
- this.log?.error(`received message for unknown session from ${msg.from}`, {
1134
- clientId: this.clientId,
1135
- transportMessage: msg,
1136
- ...conn.loggingMetadata,
1137
- tags: ["invariant-violation"]
1138
- });
1139
- return;
1140
- }
1141
- session.cancelGrace();
1142
- this.log?.debug(`received msg`, {
1143
- clientId: this.clientId,
1144
- transportMessage: msg,
1145
- ...conn.loggingMetadata
1146
- });
1147
- if (msg.seq !== session.nextExpectedSeq) {
1148
- if (msg.seq < session.nextExpectedSeq) {
1149
- this.log?.debug(
1150
- `received duplicate msg (got seq: ${msg.seq}, wanted seq: ${session.nextExpectedSeq}), discarding`,
1151
- {
1152
- clientId: this.clientId,
1153
- transportMessage: msg,
1154
- ...conn.loggingMetadata
1155
- }
1156
- );
1157
- } else {
1158
- const errMsg = `received out-of-order msg (got seq: ${msg.seq}, wanted seq: ${session.nextExpectedSeq})`;
1159
- this.log?.error(`${errMsg}, marking connection as dead`, {
1160
- clientId: this.clientId,
1161
- transportMessage: msg,
1162
- ...conn.loggingMetadata,
1163
- tags: ["invariant-violation"]
1164
- });
1165
- this.protocolError(ProtocolError.MessageOrderingViolated, errMsg);
1166
- session.telemetry.span.setStatus({
1167
- code: import_api3.SpanStatusCode.ERROR,
1168
- message: "message order violated"
1169
- });
1170
- this.deleteSession({ session, closeHandshakingConnection: true });
1171
- }
1172
- return;
1173
- }
1174
- session.updateBookkeeping(msg.ack, msg.seq);
1175
- if (!isAck(msg.controlFlags)) {
1176
- this.eventDispatcher.dispatchEvent("message", msg);
1177
- } else {
1178
- this.log?.debug(`discarding msg (ack bit set)`, {
1179
- clientId: this.clientId,
1180
- transportMessage: msg,
1181
- ...conn.loggingMetadata
1182
- });
1183
- }
1249
+ this.eventDispatcher.dispatchEvent("message", msg);
1184
1250
  }
1185
1251
  /**
1186
1252
  * Adds a listener to this transport.
@@ -1198,34 +1264,6 @@ var Transport = class {
1198
1264
  removeEventListener(type, handler) {
1199
1265
  this.eventDispatcher.removeEventListener(type, handler);
1200
1266
  }
1201
- /**
1202
- * Sends a message over this transport, delegating to the appropriate connection to actually
1203
- * send the message.
1204
- * @param msg The message to send.
1205
- * @returns The ID of the sent message or undefined if it wasn't sent
1206
- */
1207
- send(to, msg) {
1208
- if (this.getStatus() === "closed") {
1209
- const err = "transport is closed, cant send";
1210
- this.log?.error(err, {
1211
- clientId: this.clientId,
1212
- transportMessage: msg,
1213
- tags: ["invariant-violation"]
1214
- });
1215
- throw new Error(err);
1216
- }
1217
- return this.getOrCreateSession({ to }).session.send(msg);
1218
- }
1219
- // control helpers
1220
- sendCloseStream(to, streamId) {
1221
- return this.send(to, {
1222
- streamId,
1223
- controlFlags: 4 /* StreamClosedBit */,
1224
- payload: {
1225
- type: "CLOSE"
1226
- }
1227
- });
1228
- }
1229
1267
  protocolError(type, message) {
1230
1268
  this.eventDispatcher.dispatchEvent("protocolError", { type, message });
1231
1269
  }
@@ -1237,7 +1275,7 @@ var Transport = class {
1237
1275
  close() {
1238
1276
  this.status = "closed";
1239
1277
  for (const session of this.sessions.values()) {
1240
- this.deleteSession({ session, closeHandshakingConnection: true });
1278
+ this.deleteSession(session);
1241
1279
  }
1242
1280
  this.eventDispatcher.dispatchEvent("transportStatus", {
1243
1281
  status: this.status
@@ -1248,6 +1286,68 @@ var Transport = class {
1248
1286
  getStatus() {
1249
1287
  return this.status;
1250
1288
  }
1289
+ updateSession(session) {
1290
+ const activeSession = this.sessions.get(session.to);
1291
+ if (activeSession && activeSession.id !== session.id) {
1292
+ const msg = `attempt to transition active session for ${session.to} but active session (${activeSession.id}) is different from handle (${session.id})`;
1293
+ throw new Error(msg);
1294
+ }
1295
+ this.sessions.set(session.to, session);
1296
+ if (!activeSession) {
1297
+ this.eventDispatcher.dispatchEvent("sessionStatus", {
1298
+ status: "connect",
1299
+ session
1300
+ });
1301
+ }
1302
+ this.eventDispatcher.dispatchEvent("sessionTransition", {
1303
+ state: session.state,
1304
+ session
1305
+ });
1306
+ return session;
1307
+ }
1308
+ // state transitions
1309
+ deleteSession(session) {
1310
+ session.log?.info(`closing session ${session.id}`, session.loggingMetadata);
1311
+ this.eventDispatcher.dispatchEvent("sessionStatus", {
1312
+ status: "disconnect",
1313
+ session
1314
+ });
1315
+ session.close();
1316
+ this.sessions.delete(session.to);
1317
+ }
1318
+ // common listeners
1319
+ onSessionGracePeriodElapsed(session) {
1320
+ this.log?.warn(
1321
+ `session to ${session.to} grace period elapsed, closing`,
1322
+ session.loggingMetadata
1323
+ );
1324
+ this.deleteSession(session);
1325
+ }
1326
+ onConnectingFailed(session) {
1327
+ const noConnectionSession = SessionStateGraph.transition.ConnectingToNoConnection(session, {
1328
+ onSessionGracePeriodElapsed: () => {
1329
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1330
+ }
1331
+ });
1332
+ return this.updateSession(noConnectionSession);
1333
+ }
1334
+ onConnClosed(session) {
1335
+ let noConnectionSession;
1336
+ if (session.state === "Handshaking" /* Handshaking */) {
1337
+ noConnectionSession = SessionStateGraph.transition.HandshakingToNoConnection(session, {
1338
+ onSessionGracePeriodElapsed: () => {
1339
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1340
+ }
1341
+ });
1342
+ } else {
1343
+ noConnectionSession = SessionStateGraph.transition.ConnectedToNoConnection(session, {
1344
+ onSessionGracePeriodElapsed: () => {
1345
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1346
+ }
1347
+ });
1348
+ }
1349
+ return this.updateSession(noConnectionSession);
1350
+ }
1251
1351
  };
1252
1352
 
1253
1353
  // util/stringify.ts
@@ -1265,10 +1365,6 @@ var ClientTransport = class extends Transport {
1265
1365
  * The options for this transport.
1266
1366
  */
1267
1367
  options;
1268
- /**
1269
- * The map of reconnect promises for each client ID.
1270
- */
1271
- inflightConnectionPromises;
1272
1368
  retryBudget;
1273
1369
  /**
1274
1370
  * A flag indicating whether the transport should automatically reconnect
@@ -1287,352 +1383,278 @@ var ClientTransport = class extends Transport {
1287
1383
  ...defaultClientTransportOptions,
1288
1384
  ...providedOptions
1289
1385
  };
1290
- this.inflightConnectionPromises = /* @__PURE__ */ new Map();
1291
1386
  this.retryBudget = new LeakyBucketRateLimit(this.options);
1292
1387
  }
1293
1388
  extendHandshake(options) {
1294
1389
  this.handshakeExtensions = options;
1295
1390
  }
1296
- handleConnection(conn, to) {
1297
- if (this.getStatus() !== "open")
1298
- return;
1299
- let session = void 0;
1300
- const handshakeTimeout = setTimeout(() => {
1301
- if (session)
1302
- return;
1303
- this.log?.warn(
1304
- `connection to ${to} timed out waiting for handshake, closing`,
1305
- { ...conn.loggingMetadata, clientId: this.clientId, connectedTo: to }
1306
- );
1307
- conn.close();
1308
- }, this.options.handshakeTimeoutMs);
1309
- const handshakeHandler = (data) => {
1310
- const maybeSession = this.receiveHandshakeResponseMessage(data, conn);
1311
- clearTimeout(handshakeTimeout);
1312
- if (!maybeSession) {
1313
- conn.close();
1314
- return;
1315
- } else {
1316
- session = maybeSession;
1317
- }
1318
- conn.removeDataListener(handshakeHandler);
1319
- conn.addDataListener((data2) => {
1320
- const parsed = this.parseMsg(data2, conn);
1321
- if (!parsed) {
1322
- conn.telemetry?.span.setStatus({
1323
- code: import_api4.SpanStatusCode.ERROR,
1324
- message: "message parse failure"
1325
- });
1326
- conn.close();
1327
- return;
1328
- }
1329
- this.handleMsg(parsed, conn);
1391
+ tryReconnecting(to) {
1392
+ if (this.reconnectOnConnectionDrop && this.getStatus() === "open") {
1393
+ this.connect(to);
1394
+ }
1395
+ }
1396
+ send(to, msg) {
1397
+ if (this.getStatus() === "closed") {
1398
+ const err = "transport is closed, cant send";
1399
+ this.log?.error(err, {
1400
+ clientId: this.clientId,
1401
+ transportMessage: msg,
1402
+ tags: ["invariant-violation"]
1330
1403
  });
1331
- };
1332
- conn.addDataListener(handshakeHandler);
1333
- conn.addCloseListener(() => {
1334
- if (session) {
1335
- this.onDisconnect(conn, session);
1336
- }
1337
- const willReconnect = this.reconnectOnConnectionDrop && this.getStatus() === "open";
1338
- this.log?.info(
1339
- `connection to ${to} disconnected` + (willReconnect ? ", reconnecting" : ""),
1340
- {
1341
- ...conn.loggingMetadata,
1342
- ...session?.loggingMetadata,
1343
- clientId: this.clientId,
1344
- connectedTo: to
1404
+ throw new Error(err);
1405
+ }
1406
+ let session = this.sessions.get(to);
1407
+ if (!session) {
1408
+ session = this.createUnconnectedSession(to);
1409
+ }
1410
+ return session.send(msg);
1411
+ }
1412
+ createUnconnectedSession(to) {
1413
+ const session = SessionStateGraph.entrypoints.NoConnection(
1414
+ to,
1415
+ this.clientId,
1416
+ {
1417
+ onSessionGracePeriodElapsed: () => {
1418
+ this.onSessionGracePeriodElapsed(session);
1345
1419
  }
1346
- );
1347
- this.inflightConnectionPromises.delete(to);
1348
- if (this.reconnectOnConnectionDrop) {
1349
- void this.connect(to);
1420
+ },
1421
+ this.options,
1422
+ this.log
1423
+ );
1424
+ this.updateSession(session);
1425
+ return session;
1426
+ }
1427
+ // listeners
1428
+ onConnectingFailed(session) {
1429
+ const noConnectionSession = super.onConnectingFailed(session);
1430
+ this.tryReconnecting(noConnectionSession.to);
1431
+ return noConnectionSession;
1432
+ }
1433
+ onConnClosed(session) {
1434
+ const noConnectionSession = super.onConnClosed(session);
1435
+ this.tryReconnecting(noConnectionSession.to);
1436
+ return noConnectionSession;
1437
+ }
1438
+ onConnectionEstablished(session, conn) {
1439
+ const handshakingSession = SessionStateGraph.transition.ConnectingToHandshaking(session, conn, {
1440
+ onConnectionErrored: (err) => {
1441
+ const errStr = coerceErrorString(err);
1442
+ this.log?.error(
1443
+ `connection to ${handshakingSession.to} errored during handshake: ${errStr}`,
1444
+ handshakingSession.loggingMetadata
1445
+ );
1446
+ },
1447
+ onConnectionClosed: () => {
1448
+ this.log?.warn(
1449
+ `connection to ${handshakingSession.to} closed during handshake`,
1450
+ handshakingSession.loggingMetadata
1451
+ );
1452
+ this.onConnClosed(handshakingSession);
1453
+ },
1454
+ onHandshake: (msg) => {
1455
+ this.onHandshakeResponse(handshakingSession, msg);
1456
+ },
1457
+ onInvalidHandshake: (reason) => {
1458
+ this.log?.error(
1459
+ `invalid handshake: ${reason}`,
1460
+ handshakingSession.loggingMetadata
1461
+ );
1462
+ this.deleteSession(session);
1463
+ this.protocolError(ProtocolError.HandshakeFailed, reason);
1464
+ },
1465
+ onHandshakeTimeout: () => {
1466
+ this.log?.error(
1467
+ `connection to ${handshakingSession.to} timed out during handshake`,
1468
+ handshakingSession.loggingMetadata
1469
+ );
1470
+ this.onConnClosed(handshakingSession);
1350
1471
  }
1351
1472
  });
1352
- conn.addErrorListener((err) => {
1353
- conn.telemetry?.span.setStatus({
1354
- code: import_api4.SpanStatusCode.ERROR,
1355
- message: "connection error"
1356
- });
1357
- this.log?.warn(
1358
- `error in connection to ${to}: ${coerceErrorString(err)}`,
1359
- {
1360
- ...conn.loggingMetadata,
1361
- ...session?.loggingMetadata,
1362
- clientId: this.clientId,
1363
- connectedTo: to
1364
- }
1365
- );
1473
+ this.updateSession(handshakingSession);
1474
+ void this.sendHandshake(handshakingSession);
1475
+ return handshakingSession;
1476
+ }
1477
+ rejectHandshakeResponse(session, reason, metadata) {
1478
+ session.conn.telemetry?.span.setStatus({
1479
+ code: import_api3.SpanStatusCode.ERROR,
1480
+ message: reason
1366
1481
  });
1482
+ this.log?.warn(reason, metadata);
1483
+ this.deleteSession(session);
1367
1484
  }
1368
- receiveHandshakeResponseMessage(data, conn) {
1369
- const parsed = this.parseMsg(data, conn);
1370
- if (!parsed) {
1371
- conn.telemetry?.span.setStatus({
1372
- code: import_api4.SpanStatusCode.ERROR,
1373
- message: "non-transport message"
1374
- });
1375
- this.protocolError(
1376
- ProtocolError.HandshakeFailed,
1377
- "received non-transport message"
1378
- );
1379
- return false;
1380
- }
1381
- if (!import_value2.Value.Check(ControlMessageHandshakeResponseSchema, parsed.payload)) {
1382
- conn.telemetry?.span.setStatus({
1383
- code: import_api4.SpanStatusCode.ERROR,
1384
- message: "invalid handshake response"
1385
- });
1386
- this.log?.warn(`received invalid handshake resp`, {
1387
- ...conn.loggingMetadata,
1388
- clientId: this.clientId,
1389
- connectedTo: parsed.from,
1390
- transportMessage: parsed,
1485
+ onHandshakeResponse(session, msg) {
1486
+ if (!import_value2.Value.Check(ControlMessageHandshakeResponseSchema, msg.payload)) {
1487
+ const reason = `received invalid handshake response`;
1488
+ this.rejectHandshakeResponse(session, reason, {
1489
+ ...session.loggingMetadata,
1490
+ transportMessage: msg,
1391
1491
  validationErrors: [
1392
- ...import_value2.Value.Errors(
1393
- ControlMessageHandshakeResponseSchema,
1394
- parsed.payload
1395
- )
1492
+ ...import_value2.Value.Errors(ControlMessageHandshakeResponseSchema, msg.payload)
1396
1493
  ]
1397
1494
  });
1398
- this.protocolError(
1399
- ProtocolError.HandshakeFailed,
1400
- "invalid handshake resp"
1401
- );
1402
- return false;
1495
+ return;
1403
1496
  }
1404
- const previousSession = this.sessions.get(parsed.from);
1405
- if (!parsed.payload.status.ok) {
1406
- if (parsed.payload.status.reason === SESSION_STATE_MISMATCH) {
1407
- if (previousSession) {
1408
- this.deleteSession({
1409
- session: previousSession,
1410
- closeHandshakingConnection: true
1411
- });
1412
- }
1413
- conn.telemetry?.span.setStatus({
1414
- code: import_api4.SpanStatusCode.ERROR,
1415
- message: parsed.payload.status.reason
1416
- });
1497
+ if (!msg.payload.status.ok) {
1498
+ const retriable = msg.payload.status.code ? import_value2.Value.Check(
1499
+ HandshakeErrorRetriableResponseCodes,
1500
+ msg.payload.status.code
1501
+ ) : false;
1502
+ const reason = `handshake failed: ${msg.payload.status.reason}`;
1503
+ this.rejectHandshakeResponse(session, reason, {
1504
+ ...session.loggingMetadata,
1505
+ transportMessage: msg
1506
+ });
1507
+ if (retriable) {
1508
+ this.tryReconnecting(session.to);
1417
1509
  } else {
1418
- conn.telemetry?.span.setStatus({
1419
- code: import_api4.SpanStatusCode.ERROR,
1420
- message: "handshake rejected"
1421
- });
1510
+ this.deleteSession(session);
1511
+ this.protocolError(ProtocolError.HandshakeFailed, reason);
1422
1512
  }
1423
- this.log?.warn(
1424
- `received handshake rejection: ${parsed.payload.status.reason}`,
1425
- {
1426
- ...conn.loggingMetadata,
1427
- clientId: this.clientId,
1428
- connectedTo: parsed.from,
1429
- transportMessage: parsed
1430
- }
1431
- );
1432
- this.protocolError(
1433
- ProtocolError.HandshakeFailed,
1434
- parsed.payload.status.reason
1435
- );
1436
- return false;
1513
+ return;
1437
1514
  }
1438
- if (previousSession?.advertisedSessionId && previousSession.advertisedSessionId !== parsed.payload.status.sessionId) {
1439
- this.deleteSession({
1440
- session: previousSession,
1441
- closeHandshakingConnection: true
1442
- });
1443
- conn.telemetry?.span.setStatus({
1444
- code: import_api4.SpanStatusCode.ERROR,
1445
- message: "session id mismatch"
1446
- });
1447
- this.log?.warn(`handshake from ${parsed.from} session id mismatch`, {
1448
- ...conn.loggingMetadata,
1449
- clientId: this.clientId,
1450
- connectedTo: parsed.from,
1451
- transportMessage: parsed
1515
+ if (msg.payload.status.sessionId !== session.id) {
1516
+ const reason = `session id mismatch: expected ${session.id}, got ${msg.payload.status.sessionId}`;
1517
+ this.rejectHandshakeResponse(session, reason, {
1518
+ ...session.loggingMetadata,
1519
+ transportMessage: msg
1452
1520
  });
1453
- this.protocolError(ProtocolError.HandshakeFailed, "session id mismatch");
1454
- return false;
1521
+ return;
1455
1522
  }
1456
- this.log?.debug(`handshake from ${parsed.from} ok`, {
1457
- ...conn.loggingMetadata,
1458
- clientId: this.clientId,
1459
- connectedTo: parsed.from,
1460
- transportMessage: parsed
1523
+ this.log?.info(`handshake from ${msg.from} ok`, {
1524
+ ...session.loggingMetadata,
1525
+ transportMessage: msg
1461
1526
  });
1462
- const { session, isTransparentReconnect } = this.getOrCreateSession({
1463
- to: parsed.from,
1464
- conn,
1465
- sessionId: parsed.payload.status.sessionId
1527
+ const connectedSession = SessionStateGraph.transition.HandshakingToConnected(session, {
1528
+ onConnectionErrored: (err) => {
1529
+ const errStr = coerceErrorString(err);
1530
+ this.log?.warn(
1531
+ `connection to ${connectedSession.to} errored: ${errStr}`,
1532
+ connectedSession.loggingMetadata
1533
+ );
1534
+ },
1535
+ onConnectionClosed: () => {
1536
+ this.log?.info(
1537
+ `connection to ${connectedSession.to} closed`,
1538
+ connectedSession.loggingMetadata
1539
+ );
1540
+ this.onConnClosed(connectedSession);
1541
+ },
1542
+ onMessage: (msg2) => this.handleMsg(msg2),
1543
+ onInvalidMessage: (reason) => {
1544
+ this.deleteSession(connectedSession);
1545
+ this.protocolError(ProtocolError.MessageOrderingViolated, reason);
1546
+ }
1466
1547
  });
1467
- this.onConnect(conn, session, isTransparentReconnect);
1468
- this.retryBudget.startRestoringBudget(session.to);
1469
- return session;
1548
+ this.updateSession(connectedSession);
1549
+ this.retryBudget.startRestoringBudget(connectedSession.to);
1470
1550
  }
1471
1551
  /**
1472
1552
  * Manually attempts to connect to a client.
1473
1553
  * @param to The client ID of the node to connect to.
1474
1554
  */
1475
- async connect(to) {
1476
- if (this.connections.has(to)) {
1477
- this.log?.info(`already connected to ${to}, skipping connect attempt`, {
1478
- clientId: this.clientId,
1479
- connectedTo: to
1480
- });
1555
+ connect(to) {
1556
+ let session = this.sessions.get(to);
1557
+ session ??= this.createUnconnectedSession(to);
1558
+ if (session.state !== "NoConnection" /* NoConnection */) {
1559
+ this.log?.debug(
1560
+ `session to ${to} has state ${session.state}, skipping connect attempt`,
1561
+ session.loggingMetadata
1562
+ );
1481
1563
  return;
1482
1564
  }
1483
- const canProceedWithConnection = () => this.getStatus() === "open";
1484
- if (!canProceedWithConnection()) {
1565
+ if (this.getStatus() !== "open") {
1485
1566
  this.log?.info(
1486
1567
  `transport state is no longer open, cancelling attempt to connect to ${to}`,
1487
- { clientId: this.clientId, connectedTo: to }
1568
+ session.loggingMetadata
1488
1569
  );
1489
1570
  return;
1490
1571
  }
1491
- let reconnectPromise = this.inflightConnectionPromises.get(to);
1492
- if (!reconnectPromise) {
1493
- if (!this.retryBudget.hasBudget(to)) {
1494
- const budgetConsumed = this.retryBudget.getBudgetConsumed(to);
1495
- const errMsg = `tried to connect to ${to} but retry budget exceeded (more than ${budgetConsumed} attempts in the last ${this.retryBudget.totalBudgetRestoreTime}ms)`;
1496
- this.log?.error(errMsg, { clientId: this.clientId, connectedTo: to });
1497
- this.protocolError(ProtocolError.RetriesExceeded, errMsg);
1498
- return;
1499
- }
1500
- let sleep = Promise.resolve();
1501
- const backoffMs = this.retryBudget.getBackoffMs(to);
1502
- if (backoffMs > 0) {
1503
- sleep = new Promise((resolve) => setTimeout(resolve, backoffMs));
1504
- }
1505
- this.log?.info(
1506
- `attempting connection to ${to} (${backoffMs}ms backoff)`,
1507
- {
1508
- clientId: this.clientId,
1509
- connectedTo: to
1510
- }
1511
- );
1512
- this.retryBudget.consumeBudget(to);
1513
- reconnectPromise = tracing_default.startActiveSpan("connect", async (span) => {
1514
- try {
1515
- span.addEvent("backoff", { backoffMs });
1516
- await sleep;
1517
- if (!canProceedWithConnection()) {
1518
- throw new Error("transport state is no longer open");
1519
- }
1520
- span.addEvent("connecting");
1521
- const conn = await this.createNewOutgoingConnection(to);
1522
- if (!canProceedWithConnection()) {
1523
- this.log?.info(
1524
- `transport state is no longer open, closing pre-handshake connection to ${to}`,
1525
- {
1526
- ...conn.loggingMetadata,
1527
- clientId: this.clientId,
1528
- connectedTo: to
1529
- }
1530
- );
1531
- conn.close();
1532
- throw new Error("transport state is no longer open");
1533
- }
1534
- span.addEvent("sending handshake");
1535
- const ok = await this.sendHandshake(to, conn);
1536
- if (!ok) {
1537
- conn.close();
1538
- throw new Error("failed to send handshake");
1539
- }
1540
- return conn;
1541
- } catch (err) {
1542
- const errStr = coerceErrorString(err);
1543
- span.recordException(errStr);
1544
- span.setStatus({ code: import_api4.SpanStatusCode.ERROR });
1545
- throw err;
1546
- } finally {
1547
- span.end();
1548
- }
1549
- });
1550
- this.inflightConnectionPromises.set(to, reconnectPromise);
1551
- } else {
1552
- this.log?.info(
1553
- `attempting connection to ${to} (reusing previous attempt)`,
1554
- {
1555
- clientId: this.clientId,
1556
- connectedTo: to
1557
- }
1558
- );
1572
+ if (!this.retryBudget.hasBudget(to)) {
1573
+ const budgetConsumed = this.retryBudget.getBudgetConsumed(to);
1574
+ const errMsg = `tried to connect to ${to} but retry budget exceeded (more than ${budgetConsumed} attempts in the last ${this.retryBudget.totalBudgetRestoreTime}ms)`;
1575
+ this.log?.error(errMsg, session.loggingMetadata);
1576
+ this.protocolError(ProtocolError.RetriesExceeded, errMsg);
1577
+ return;
1559
1578
  }
1560
- try {
1561
- await reconnectPromise;
1562
- } catch (error) {
1563
- this.inflightConnectionPromises.delete(to);
1564
- const errStr = coerceErrorString(error);
1565
- if (!this.reconnectOnConnectionDrop || !canProceedWithConnection()) {
1566
- this.log?.warn(`connection to ${to} failed (${errStr})`, {
1567
- clientId: this.clientId,
1568
- connectedTo: to
1569
- });
1570
- } else {
1571
- this.log?.warn(`connection to ${to} failed (${errStr}), retrying`, {
1572
- clientId: this.clientId,
1573
- connectedTo: to
1574
- });
1575
- await this.connect(to);
1576
- }
1579
+ let sleep = Promise.resolve();
1580
+ const backoffMs = this.retryBudget.getBackoffMs(to);
1581
+ if (backoffMs > 0) {
1582
+ sleep = new Promise((resolve) => setTimeout(resolve, backoffMs));
1577
1583
  }
1578
- }
1579
- deleteSession({
1580
- session,
1581
- closeHandshakingConnection,
1582
- handshakingConn
1583
- }) {
1584
- this.inflightConnectionPromises.delete(session.to);
1585
- super.deleteSession({
1586
- session,
1587
- closeHandshakingConnection,
1588
- handshakingConn
1584
+ this.log?.info(
1585
+ `attempting connection to ${to} (${backoffMs}ms backoff)`,
1586
+ session.loggingMetadata
1587
+ );
1588
+ this.retryBudget.consumeBudget(to);
1589
+ const reconnectPromise = tracing_default.startActiveSpan("connect", async (span) => {
1590
+ try {
1591
+ span.addEvent("backoff", { backoffMs });
1592
+ await sleep;
1593
+ if (this.getStatus() !== "open") {
1594
+ throw new Error("transport state is no longer open");
1595
+ }
1596
+ span.addEvent("connecting");
1597
+ return await this.createNewOutgoingConnection(to);
1598
+ } catch (err) {
1599
+ const errStr = coerceErrorString(err);
1600
+ span.recordException(errStr);
1601
+ span.setStatus({ code: import_api3.SpanStatusCode.ERROR });
1602
+ throw err;
1603
+ } finally {
1604
+ span.end();
1605
+ }
1589
1606
  });
1607
+ const connectingSession = SessionStateGraph.transition.NoConnectionToConnecting(
1608
+ session,
1609
+ reconnectPromise,
1610
+ {
1611
+ onConnectionEstablished: (conn) => {
1612
+ this.log?.debug(
1613
+ `connection to ${connectingSession.to} established`,
1614
+ connectingSession.loggingMetadata
1615
+ );
1616
+ this.onConnectionEstablished(connectingSession, conn);
1617
+ },
1618
+ onConnectionFailed: (error) => {
1619
+ const errStr = coerceErrorString(error);
1620
+ this.log?.error(
1621
+ `error connecting to ${connectingSession.to}: ${errStr}`,
1622
+ connectingSession.loggingMetadata
1623
+ );
1624
+ this.onConnectingFailed(connectingSession);
1625
+ },
1626
+ onConnectionTimeout: () => {
1627
+ this.log?.error(
1628
+ `connection to ${connectingSession.to} timed out`,
1629
+ connectingSession.loggingMetadata
1630
+ );
1631
+ this.onConnectingFailed(connectingSession);
1632
+ }
1633
+ }
1634
+ );
1635
+ this.updateSession(connectingSession);
1590
1636
  }
1591
- async sendHandshake(to, conn) {
1637
+ async sendHandshake(session) {
1592
1638
  let metadata = void 0;
1593
1639
  if (this.handshakeExtensions) {
1594
1640
  metadata = await this.handshakeExtensions.construct();
1595
- if (!import_value2.Value.Check(this.handshakeExtensions.schema, metadata)) {
1596
- this.log?.error(`constructed handshake metadata did not match schema`, {
1597
- ...conn.loggingMetadata,
1598
- clientId: this.clientId,
1599
- connectedTo: to,
1600
- validationErrors: [
1601
- ...import_value2.Value.Errors(this.handshakeExtensions.schema, metadata)
1602
- ],
1603
- tags: ["invariant-violation"]
1604
- });
1605
- this.protocolError(
1606
- ProtocolError.HandshakeFailed,
1607
- "handshake metadata did not match schema"
1608
- );
1609
- conn.telemetry?.span.setStatus({
1610
- code: import_api4.SpanStatusCode.ERROR,
1611
- message: "handshake meta mismatch"
1612
- });
1613
- return false;
1614
- }
1615
1641
  }
1616
- const { session } = this.getOrCreateSession({ to, handshakingConn: conn });
1617
1642
  const requestMsg = handshakeRequestMessage({
1618
1643
  from: this.clientId,
1619
- to,
1644
+ to: session.to,
1620
1645
  sessionId: session.id,
1621
1646
  expectedSessionState: {
1622
- reconnect: session.advertisedSessionId !== void 0,
1623
- nextExpectedSeq: session.nextExpectedSeq
1647
+ nextExpectedSeq: session.ack,
1648
+ nextSentSeq: session.nextSeq()
1624
1649
  },
1625
1650
  metadata,
1626
1651
  tracing: getPropagationContext(session.telemetry.ctx)
1627
1652
  });
1628
- this.log?.debug(`sending handshake request to ${to}`, {
1629
- ...conn.loggingMetadata,
1630
- clientId: this.clientId,
1631
- connectedTo: to,
1653
+ this.log?.debug(`sending handshake request to ${session.to}`, {
1654
+ ...session.loggingMetadata,
1632
1655
  transportMessage: requestMsg
1633
1656
  });
1634
- conn.send(this.codec.toBuffer(requestMsg));
1635
- return true;
1657
+ session.sendHandshake(requestMsg);
1636
1658
  }
1637
1659
  close() {
1638
1660
  this.retryBudget.close();
@@ -1648,10 +1670,6 @@ var UnixDomainSocketClientTransport = class extends ClientTransport {
1648
1670
  this.path = socketPath;
1649
1671
  }
1650
1672
  async createNewOutgoingConnection(to) {
1651
- const oldConnection = this.connections.get(to);
1652
- if (oldConnection) {
1653
- oldConnection.close();
1654
- }
1655
1673
  this.log?.info(`establishing a new uds to ${to}`, {
1656
1674
  clientId: this.clientId,
1657
1675
  connectedTo: to
@@ -1662,9 +1680,7 @@ var UnixDomainSocketClientTransport = class extends ClientTransport {
1662
1680
  sock2.on("error", (err) => reject(err));
1663
1681
  sock2.connect(this.path);
1664
1682
  });
1665
- const conn = new UdsConnection(sock);
1666
- this.handleConnection(conn, to);
1667
- return conn;
1683
+ return new UdsConnection(sock);
1668
1684
  }
1669
1685
  };
1670
1686
  // Annotate the CommonJS export names for ESM import in node: