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