@replit/river 0.23.16 → 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 (85) hide show
  1. package/README.md +21 -20
  2. package/dist/{chunk-UDXM64QK.js → chunk-AASMR3CQ.js} +24 -18
  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-LTSLICON.js → chunk-KYYB4DUR.js} +68 -519
  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-TXSQRTZB.js → chunk-PJGGC3LV.js} +55 -41
  13. package/dist/chunk-PJGGC3LV.js.map +1 -0
  14. package/dist/chunk-RXJLI2OP.js +50 -0
  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-0926d3d6.d.ts → client-ba0d3315.d.ts} +12 -15
  21. package/dist/{connection-99a67d3e.d.ts → connection-c3a96d09.d.ts} +1 -5
  22. package/dist/connection-d33e3246.d.ts +11 -0
  23. package/dist/{handshake-75d0124f.d.ts → handshake-cdead82a.d.ts} +149 -180
  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 +107 -530
  30. package/dist/router/index.cjs.map +1 -1
  31. package/dist/router/index.d.cts +12 -50
  32. package/dist/router/index.d.ts +12 -50
  33. package/dist/router/index.js +2 -4
  34. package/dist/server-2ef5e6ec.d.ts +42 -0
  35. package/dist/{services-75e84a9f.d.ts → services-e1417b33.d.ts} +7 -7
  36. package/dist/transport/impls/uds/client.cjs +1242 -1223
  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 +1301 -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 +980 -969
  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 +1434 -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 -309
  64. package/dist/util/testHelpers.cjs.map +1 -1
  65. package/dist/util/testHelpers.d.cts +10 -7
  66. package/dist/util/testHelpers.d.ts +10 -7
  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-JA7XGTAL.js +0 -476
  72. package/dist/chunk-JA7XGTAL.js.map +0 -1
  73. package/dist/chunk-LTSLICON.js.map +0 -1
  74. package/dist/chunk-MQCGG6KL.js +0 -335
  75. package/dist/chunk-MQCGG6KL.js.map +0 -1
  76. package/dist/chunk-R47IZD67.js +0 -59
  77. package/dist/chunk-R47IZD67.js.map +0 -1
  78. package/dist/chunk-TXSQRTZB.js.map +0 -1
  79. package/dist/chunk-UDXM64QK.js.map +0 -1
  80. package/dist/chunk-WN77AT67.js +0 -476
  81. package/dist/chunk-WN77AT67.js.map +0 -1
  82. package/dist/chunk-YXDAOVP7.js +0 -347
  83. package/dist/chunk-YXDAOVP7.js.map +0 -1
  84. package/dist/connection-d738cc08.d.ts +0 -17
  85. package/dist/server-3740c5d9.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,974 +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.16";
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);
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);
652
+ }
653
+ );
484
654
  }
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;
503
- }
504
- }
505
- cb();
506
- }
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();
513
- }
514
- _destroy(error, callback) {
515
- this.receivedBuffer = Buffer.alloc(0);
516
- super._destroy(error, callback);
517
- }
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]);
531
- }
532
- };
533
-
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);
553
- }
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);
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(() => {
560
659
  });
561
660
  }
562
- send(payload) {
563
- if (this.framer.destroyed || !this.sock.writable)
564
- return false;
565
- return this.sock.write(MessageFramer.write(payload));
661
+ _handleStateExit() {
662
+ super._handleStateExit();
663
+ clearTimeout(this.connectionTimeout);
664
+ this.connectionTimeout = void 0;
566
665
  }
567
- close() {
568
- this.sock.destroy();
569
- this.framer.destroy();
666
+ _handleClose() {
667
+ this.bestEffortClose();
668
+ super._handleClose();
570
669
  }
571
670
  };
572
671
 
573
- // transport/server.ts
574
- var import_api4 = require("@opentelemetry/api");
575
-
576
- // codec/json.ts
577
- var encoder = new TextEncoder();
578
- var decoder = new TextDecoder();
579
- function uint8ArrayToBase64(uint8Array) {
580
- let binary = "";
581
- uint8Array.forEach((byte) => {
582
- binary += String.fromCharCode(byte);
583
- });
584
- return btoa(binary);
585
- }
586
- function base64ToUint8Array(base64) {
587
- const binaryString = atob(base64);
588
- const uint8Array = new Uint8Array(binaryString.length);
589
- for (let i = 0; i < binaryString.length; i++) {
590
- uint8Array[i] = binaryString.charCodeAt(i);
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);
591
683
  }
592
- return uint8Array;
593
- }
594
- var NaiveJsonCodec = {
595
- toBuffer: (obj) => {
596
- return encoder.encode(
597
- JSON.stringify(obj, function replacer(key) {
598
- const val = this[key];
599
- if (val instanceof Uint8Array) {
600
- return { $t: uint8ArrayToBase64(val) };
601
- } else {
602
- return val;
603
- }
604
- })
605
- );
606
- },
607
- fromBuffer: (buff) => {
608
- try {
609
- const parsed = JSON.parse(
610
- decoder.decode(buff),
611
- function reviver(_key, val) {
612
- if (val?.$t) {
613
- return base64ToUint8Array(val.$t);
614
- } else {
615
- return val;
616
- }
617
- }
618
- );
619
- if (typeof parsed === "object")
620
- return parsed;
621
- return null;
622
- } catch {
623
- return null;
684
+ _handleClose() {
685
+ super._handleClose();
686
+ }
687
+ _handleStateExit() {
688
+ super._handleStateExit();
689
+ if (this.gracePeriodTimeout) {
690
+ clearTimeout(this.gracePeriodTimeout);
691
+ this.gracePeriodTimeout = void 0;
624
692
  }
625
693
  }
626
694
  };
627
695
 
628
- // transport/options.ts
629
- var defaultTransportOptions = {
630
- heartbeatIntervalMs: 1e3,
631
- heartbeatsUntilDead: 2,
632
- sessionDisconnectGraceMs: 5e3,
633
- codec: NaiveJsonCodec
634
- };
635
- var defaultConnectionRetryOptions = {
636
- baseIntervalMs: 250,
637
- maxJitterMs: 200,
638
- maxBackoffMs: 32e3,
639
- attemptBudgetCapacity: 5,
640
- budgetRestoreIntervalMs: 200
641
- };
642
- var defaultClientTransportOptions = {
643
- ...defaultTransportOptions,
644
- ...defaultConnectionRetryOptions
645
- };
646
- var defaultServerTransportOptions = {
647
- ...defaultTransportOptions
648
- };
696
+ // tracing/index.ts
697
+ var import_api = require("@opentelemetry/api");
649
698
 
650
- // transport/transport.ts
651
- var import_value = require("@sinclair/typebox/value");
699
+ // package.json
700
+ var version = "0.24.0";
652
701
 
653
- // logging/log.ts
654
- var LoggingLevels = {
655
- debug: -1,
656
- info: 0,
657
- warn: 1,
658
- error: 2
659
- };
660
- var cleanedLogFn = (log) => {
661
- return (msg, metadata) => {
662
- if (!metadata?.transportMessage) {
663
- log(msg, metadata);
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);
721
+
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");
664
743
  return;
665
744
  }
666
- const { payload, ...rest } = metadata.transportMessage;
667
- metadata.transportMessage = rest;
668
- log(msg, metadata);
745
+ this.listeners.onHandshake(parsedMsg);
669
746
  };
670
- };
671
- var BaseLogger = class {
672
- minLevel;
673
- output;
674
- constructor(output, minLevel = "info") {
675
- this.minLevel = minLevel;
676
- this.output = output;
677
- }
678
- debug(msg, metadata) {
679
- if (LoggingLevels[this.minLevel] <= LoggingLevels.debug) {
680
- this.output(msg, metadata ?? {}, "debug");
681
- }
747
+ get loggingMetadata() {
748
+ return {
749
+ clientId: this.from,
750
+ connId: this.conn.id
751
+ };
682
752
  }
683
- info(msg, metadata) {
684
- if (LoggingLevels[this.minLevel] <= LoggingLevels.info) {
685
- this.output(msg, metadata ?? {}, "info");
686
- }
753
+ sendHandshake(msg) {
754
+ return this.conn.send(this.options.codec.toBuffer(msg));
687
755
  }
688
- warn(msg, metadata) {
689
- if (LoggingLevels[this.minLevel] <= LoggingLevels.warn) {
690
- this.output(msg, metadata ?? {}, "warn");
691
- }
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;
692
762
  }
693
- error(msg, metadata) {
694
- if (LoggingLevels[this.minLevel] <= LoggingLevels.error) {
695
- this.output(msg, metadata ?? {}, "error");
696
- }
763
+ _handleClose() {
764
+ this.conn.close();
697
765
  }
698
766
  };
699
- var createLogProxy = (log) => ({
700
- debug: cleanedLogFn(log.debug.bind(log)),
701
- info: cleanedLogFn(log.info.bind(log)),
702
- warn: cleanedLogFn(log.warn.bind(log)),
703
- error: cleanedLogFn(log.error.bind(log))
704
- });
705
767
 
706
- // transport/events.ts
707
- var ProtocolError = {
708
- RetriesExceeded: "conn_retry_exceeded",
709
- HandshakeFailed: "handshake_failed",
710
- MessageOrderingViolated: "message_ordering_violated"
711
- };
712
- var EventDispatcher = class {
713
- eventListeners = {};
714
- removeAllListeners() {
715
- this.eventListeners = {};
716
- }
717
- numberOfListeners(eventType) {
718
- return this.eventListeners[eventType]?.size ?? 0;
719
- }
720
- addEventListener(eventType, handler) {
721
- if (!this.eventListeners[eventType]) {
722
- this.eventListeners[eventType] = /* @__PURE__ */ new Set();
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);
784
+ }
785
+ onHandshakeData = (msg) => {
786
+ const parsedMsg = this.parseMsg(msg);
787
+ if (parsedMsg === null) {
788
+ this.listeners.onInvalidHandshake("could not parse message");
789
+ return;
723
790
  }
724
- this.eventListeners[eventType]?.add(handler);
791
+ this.listeners.onHandshake(parsedMsg);
792
+ };
793
+ sendHandshake(msg) {
794
+ return this.conn.send(this.options.codec.toBuffer(msg));
725
795
  }
726
- removeEventListener(eventType, handler) {
727
- const handlers = this.eventListeners[eventType];
728
- if (handlers) {
729
- this.eventListeners[eventType]?.delete(handler);
730
- }
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);
731
802
  }
732
- dispatchEvent(eventType, event) {
733
- const handlers = this.eventListeners[eventType];
734
- if (handlers) {
735
- const copy = [...handlers];
736
- for (const handler of copy) {
737
- handler(event);
738
- }
739
- }
803
+ _handleClose() {
804
+ super._handleClose();
805
+ this.conn.close();
740
806
  }
741
807
  };
742
808
 
743
- // transport/transport.ts
744
- var import_api3 = require("@opentelemetry/api");
745
- var Transport = class {
746
- /**
747
- * The status of the transport.
748
- */
749
- status;
750
- /**
751
- * The {@link Codec} used to encode and decode messages.
752
- */
753
- codec;
754
- /**
755
- * The client ID of this transport.
756
- */
757
- clientId;
758
- /**
759
- * The map of {@link Session}s managed by this transport.
760
- */
761
- sessions;
762
- /**
763
- * The map of {@link Connection}s managed by this transport.
764
- */
765
- get connections() {
766
- return new Map(
767
- [...this.sessions].map(([client, session]) => [client, session.connection]).filter((entry) => entry[1] !== void 0)
768
- );
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;
769
819
  }
770
- /**
771
- * The event dispatcher for handling events of type EventTypes.
772
- */
773
- eventDispatcher;
774
- /**
775
- * The options for this transport.
776
- */
777
- options;
778
- log;
779
- /**
780
- * Creates a new Transport instance.
781
- * This should also set up {@link onConnect}, and {@link onDisconnect} listeners.
782
- * @param codec The codec used to encode and decode messages.
783
- * @param clientId The client ID of this transport.
784
- */
785
- constructor(clientId, providedOptions) {
786
- this.options = { ...defaultTransportOptions, ...providedOptions };
787
- this.eventDispatcher = new EventDispatcher();
788
- this.sessions = /* @__PURE__ */ new Map();
789
- this.codec = this.options.codec;
790
- this.clientId = clientId;
791
- 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;
792
824
  }
793
- bindLogger(fn, level) {
794
- if (typeof fn === "function") {
795
- this.log = createLogProxy(new BaseLogger(fn, level));
796
- 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));
797
846
  }
798
- this.log = createLogProxy(fn);
799
847
  }
800
- /**
801
- * Called when a new connection is established
802
- * and we know the identity of the connected client.
803
- * @param conn The connection object.
804
- */
805
- onConnect(conn, session, isTransparentReconnect) {
806
- this.eventDispatcher.dispatchEvent("connectionStatus", {
807
- status: "connect",
808
- conn
809
- });
810
- conn.telemetry = createConnectionTelemetryInfo(conn, session.telemetry);
811
- session.replaceWithNewConnection(conn, isTransparentReconnect);
812
- this.log?.info(`connected to ${session.to}`, {
813
- ...conn.loggingMetadata,
814
- ...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
+ }
815
874
  });
816
875
  }
817
- createSession(to, conn, propagationCtx) {
818
- const session = new Session(
819
- conn,
820
- this.clientId,
821
- to,
822
- this.options,
823
- propagationCtx
824
- );
825
- if (this.log) {
826
- session.bindLogger(this.log);
827
- }
828
- const currentSession = this.sessions.get(session.to);
829
- if (currentSession) {
830
- this.log?.warn(
831
- `session ${session.id} from ${session.to} surreptitiously replacing ${currentSession.id}`,
832
- {
833
- ...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,
834
894
  tags: ["invariant-violation"]
835
- }
836
- );
837
- this.deleteSession({
838
- session: currentSession,
839
- closeHandshakingConnection: false
840
- });
895
+ });
896
+ this.telemetry.span.setStatus({
897
+ code: import_api2.SpanStatusCode.ERROR,
898
+ message: reason
899
+ });
900
+ this.listeners.onInvalidMessage(reason);
901
+ }
902
+ return;
841
903
  }
842
- this.sessions.set(session.to, session);
843
- this.eventDispatcher.dispatchEvent("sessionStatus", {
844
- status: "connect",
845
- session
904
+ this.log?.debug(`received msg`, {
905
+ ...this.loggingMetadata,
906
+ transportMessage: parsedMsg
846
907
  });
847
- return session;
848
- }
849
- createNewSession({
850
- to,
851
- conn,
852
- sessionId,
853
- propagationCtx
854
- }) {
855
- let session = this.sessions.get(to);
856
- if (session !== void 0) {
857
- this.log?.info(
858
- `session for ${to} already exists, replacing it with a new session as requested`,
859
- session.loggingMetadata
860
- );
861
- this.deleteSession({
862
- session,
863
- closeHandshakingConnection: false
864
- });
865
- session = void 0;
908
+ this.updateBookkeeping(parsedMsg.ack, parsedMsg.seq);
909
+ if (!isAck(parsedMsg.controlFlags)) {
910
+ this.listeners.onMessage(parsedMsg);
911
+ return;
866
912
  }
867
- session = this.createSession(to, conn, propagationCtx);
868
- session.advertisedSessionId = sessionId;
869
- this.log?.info(`created new session for ${to}`, session.loggingMetadata);
870
- return session;
871
- }
872
- getExistingSession({
873
- to,
874
- sessionId,
875
- nextExpectedSeq
876
- }) {
877
- const session = this.sessions.get(to);
878
- if (
879
- // reject this request if there was no previous session to replace
880
- session === void 0 || // or if both parties do not agree about the next expected sequence number
881
- !session.nextExpectedSeqInRange(nextExpectedSeq) || // or if both parties do not agree on the advertised session id
882
- session.advertisedSessionId !== sessionId
883
- ) {
884
- 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();
885
919
  }
886
- this.log?.info(
887
- `reused existing session for ${to}`,
888
- session.loggingMetadata
889
- );
890
- 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();
891
932
  }
892
- getOrCreateSession({
893
- to,
894
- conn,
895
- handshakingConn,
896
- sessionId,
897
- propagationCtx
898
- }) {
899
- let session = this.sessions.get(to);
900
- const isReconnect = session !== void 0;
901
- let isTransparentReconnect = isReconnect;
902
- if (session?.advertisedSessionId !== void 0 && sessionId !== void 0 && session.advertisedSessionId !== sessionId) {
903
- this.log?.info(
904
- `session for ${to} already exists but has a different session id (expected: ${session.advertisedSessionId}, got: ${sessionId}), creating a new one`,
905
- 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
906
966
  );
907
- this.deleteSession({
908
- session,
909
- closeHandshakingConnection: handshakingConn !== void 0,
910
- handshakingConn
967
+ session.log?.info(`session ${session.id} created in NoConnection state`, {
968
+ ...session.loggingMetadata,
969
+ tags: ["state-transition"]
911
970
  });
912
- isTransparentReconnect = false;
913
- session = void 0;
914
- }
915
- if (!session) {
916
- session = this.createSession(to, conn, propagationCtx);
917
- this.log?.info(
918
- `no session for ${to}, created a new one`,
919
- 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
920
980
  );
981
+ session.log?.info(`session created in WaitingForHandshake state`, {
982
+ ...session.loggingMetadata,
983
+ tags: ["state-transition"]
984
+ });
985
+ return session;
921
986
  }
922
- if (sessionId !== void 0) {
923
- session.advertisedSessionId = sessionId;
924
- }
925
- if (handshakingConn !== void 0) {
926
- session.replaceWithNewHandshakingConnection(handshakingConn);
927
- }
928
- return { session, isReconnect, isTransparentReconnect };
929
- }
930
- deleteSession({
931
- session,
932
- closeHandshakingConnection,
933
- handshakingConn
934
- }) {
935
- if (closeHandshakingConnection) {
936
- session.closeHandshakingConnection(handshakingConn);
937
- }
938
- session.close();
939
- session.telemetry.span.end();
940
- const currentSession = this.sessions.get(session.to);
941
- if (currentSession && currentSession.id !== session.id) {
942
- this.log?.warn(
943
- `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`,
944
1002
  {
945
1003
  ...session.loggingMetadata,
946
- tags: ["invariant-violation"]
1004
+ tags: ["state-transition"]
947
1005
  }
948
1006
  );
949
- 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;
950
1110
  }
951
- this.sessions.delete(session.to);
952
- this.log?.info(
953
- `session ${session.id} disconnect from ${session.to}`,
954
- session.loggingMetadata
955
- );
956
- this.eventDispatcher.dispatchEvent("sessionStatus", {
957
- status: "disconnect",
958
- session
959
- });
960
1111
  }
1112
+ };
1113
+
1114
+ // transport/transport.ts
1115
+ var Transport = class {
961
1116
  /**
962
- * The downstream implementation needs to call this when a connection is closed.
963
- * @param conn The connection object.
964
- * @param connectedTo The peer we are connected to.
1117
+ * The status of the transport.
965
1118
  */
966
- onDisconnect(conn, session) {
967
- if (session.connection !== void 0 && session.connection.id !== conn.id) {
968
- session.telemetry.span.addEvent("onDisconnect race");
969
- this.log?.warn("onDisconnect race", {
970
- clientId: this.clientId,
971
- ...session.loggingMetadata,
972
- ...conn.loggingMetadata,
973
- tags: ["invariant-violation"]
974
- });
975
- return;
976
- }
977
- conn.telemetry?.span.end();
978
- this.eventDispatcher.dispatchEvent("connectionStatus", {
979
- status: "disconnect",
980
- conn
981
- });
982
- session.connection = void 0;
983
- session.beginGrace(() => {
984
- if (session.connection !== void 0) {
985
- session.telemetry.span.addEvent("session grace period race");
986
- this.log?.warn("session grace period race", {
987
- clientId: this.clientId,
988
- ...session.loggingMetadata,
989
- ...conn.loggingMetadata,
990
- tags: ["invariant-violation"]
991
- });
992
- return;
993
- }
994
- session.telemetry.span.addEvent("session grace period expired");
995
- this.deleteSession({
996
- session,
997
- closeHandshakingConnection: true,
998
- handshakingConn: conn
999
- });
1000
- });
1001
- }
1119
+ status;
1002
1120
  /**
1003
- * Parses a message from a Uint8Array into a {@link OpaqueTransportMessage}.
1004
- * @param msg The message to parse.
1005
- * @returns The parsed message, or null if the message is malformed or invalid.
1121
+ * The client ID of this transport.
1006
1122
  */
1007
- parseMsg(msg, conn) {
1008
- const parsedMsg = this.codec.fromBuffer(msg);
1009
- if (parsedMsg === null) {
1010
- const decodedBuffer = new TextDecoder().decode(Buffer.from(msg));
1011
- this.log?.error(
1012
- `received malformed msg, killing conn: ${decodedBuffer}`,
1013
- {
1014
- clientId: this.clientId,
1015
- ...conn.loggingMetadata
1016
- }
1017
- );
1018
- return null;
1019
- }
1020
- if (!import_value.Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
1021
- this.log?.error(`received invalid msg: ${JSON.stringify(parsedMsg)}`, {
1022
- clientId: this.clientId,
1023
- ...conn.loggingMetadata,
1024
- validationErrors: [
1025
- ...import_value.Value.Errors(OpaqueTransportMessageSchema, parsedMsg)
1026
- ]
1027
- });
1028
- 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;
1029
1150
  }
1030
- return parsedMsg;
1151
+ this.log = createLogProxy(fn);
1031
1152
  }
1032
1153
  /**
1033
1154
  * Called when a message is received by this transport.
1034
1155
  * You generally shouldn't need to override this in downstream transport implementations.
1035
1156
  * @param msg The received message.
1036
1157
  */
1037
- handleMsg(msg, conn) {
1158
+ handleMsg(msg) {
1038
1159
  if (this.getStatus() !== "open")
1039
1160
  return;
1040
- const session = this.sessions.get(msg.from);
1041
- if (!session) {
1042
- this.log?.error(`received message for unknown session from ${msg.from}`, {
1043
- clientId: this.clientId,
1044
- transportMessage: msg,
1045
- ...conn.loggingMetadata,
1046
- tags: ["invariant-violation"]
1047
- });
1048
- return;
1049
- }
1050
- session.cancelGrace();
1051
- this.log?.debug(`received msg`, {
1052
- clientId: this.clientId,
1053
- transportMessage: msg,
1054
- ...conn.loggingMetadata
1055
- });
1056
- if (msg.seq !== session.nextExpectedSeq) {
1057
- if (msg.seq < session.nextExpectedSeq) {
1058
- this.log?.debug(
1059
- `received duplicate msg (got seq: ${msg.seq}, wanted seq: ${session.nextExpectedSeq}), discarding`,
1060
- {
1061
- clientId: this.clientId,
1062
- transportMessage: msg,
1063
- ...conn.loggingMetadata
1064
- }
1065
- );
1066
- } else {
1067
- const errMsg = `received out-of-order msg (got seq: ${msg.seq}, wanted seq: ${session.nextExpectedSeq})`;
1068
- this.log?.error(`${errMsg}, marking connection as dead`, {
1069
- clientId: this.clientId,
1070
- transportMessage: msg,
1071
- ...conn.loggingMetadata,
1072
- tags: ["invariant-violation"]
1073
- });
1074
- this.protocolError(ProtocolError.MessageOrderingViolated, errMsg);
1075
- session.telemetry.span.setStatus({
1076
- code: import_api3.SpanStatusCode.ERROR,
1077
- message: "message order violated"
1078
- });
1079
- this.deleteSession({ session, closeHandshakingConnection: true });
1080
- }
1081
- return;
1082
- }
1083
- session.updateBookkeeping(msg.ack, msg.seq);
1084
- if (!isAck(msg.controlFlags)) {
1085
- this.eventDispatcher.dispatchEvent("message", msg);
1086
- } else {
1087
- this.log?.debug(`discarding msg (ack bit set)`, {
1088
- clientId: this.clientId,
1089
- transportMessage: msg,
1090
- ...conn.loggingMetadata
1091
- });
1092
- }
1161
+ this.eventDispatcher.dispatchEvent("message", msg);
1093
1162
  }
1094
1163
  /**
1095
1164
  * Adds a listener to this transport.
@@ -1107,34 +1176,6 @@ var Transport = class {
1107
1176
  removeEventListener(type, handler) {
1108
1177
  this.eventDispatcher.removeEventListener(type, handler);
1109
1178
  }
1110
- /**
1111
- * Sends a message over this transport, delegating to the appropriate connection to actually
1112
- * send the message.
1113
- * @param msg The message to send.
1114
- * @returns The ID of the sent message or undefined if it wasn't sent
1115
- */
1116
- send(to, msg) {
1117
- if (this.getStatus() === "closed") {
1118
- const err = "transport is closed, cant send";
1119
- this.log?.error(err, {
1120
- clientId: this.clientId,
1121
- transportMessage: msg,
1122
- tags: ["invariant-violation"]
1123
- });
1124
- throw new Error(err);
1125
- }
1126
- return this.getOrCreateSession({ to }).session.send(msg);
1127
- }
1128
- // control helpers
1129
- sendCloseStream(to, streamId) {
1130
- return this.send(to, {
1131
- streamId,
1132
- controlFlags: 4 /* StreamClosedBit */,
1133
- payload: {
1134
- type: "CLOSE"
1135
- }
1136
- });
1137
- }
1138
1179
  protocolError(type, message) {
1139
1180
  this.eventDispatcher.dispatchEvent("protocolError", { type, message });
1140
1181
  }
@@ -1146,7 +1187,7 @@ var Transport = class {
1146
1187
  close() {
1147
1188
  this.status = "closed";
1148
1189
  for (const session of this.sessions.values()) {
1149
- this.deleteSession({ session, closeHandshakingConnection: true });
1190
+ this.deleteSession(session);
1150
1191
  }
1151
1192
  this.eventDispatcher.dispatchEvent("transportStatus", {
1152
1193
  status: this.status
@@ -1157,6 +1198,68 @@ var Transport = class {
1157
1198
  getStatus() {
1158
1199
  return this.status;
1159
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
+ }
1160
1263
  };
1161
1264
 
1162
1265
  // util/stringify.ts
@@ -1181,14 +1284,14 @@ var ServerTransport = class extends Transport {
1181
1284
  /**
1182
1285
  * A map of session handshake data for each session.
1183
1286
  */
1184
- sessionHandshakeMetadata;
1287
+ sessionHandshakeMetadata = /* @__PURE__ */ new Map();
1288
+ pendingSessions = /* @__PURE__ */ new Set();
1185
1289
  constructor(clientId, providedOptions) {
1186
1290
  super(clientId, providedOptions);
1187
1291
  this.options = {
1188
1292
  ...defaultServerTransportOptions,
1189
1293
  ...providedOptions
1190
1294
  };
1191
- this.sessionHandshakeMetadata = /* @__PURE__ */ new WeakMap();
1192
1295
  this.log?.info(`initiated server transport`, {
1193
1296
  clientId: this.clientId,
1194
1297
  protocolVersion: PROTOCOL_VERSION
@@ -1197,6 +1300,36 @@ var ServerTransport = class extends Transport {
1197
1300
  extendHandshake(options) {
1198
1301
  this.handshakeExtensions = options;
1199
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
+ }
1200
1333
  handleConnection(conn) {
1201
1334
  if (this.getStatus() !== "open")
1202
1335
  return;
@@ -1204,281 +1337,298 @@ var ServerTransport = class extends Transport {
1204
1337
  ...conn.loggingMetadata,
1205
1338
  clientId: this.clientId
1206
1339
  });
1207
- let session = void 0;
1208
- const client = () => session?.to ?? "unknown";
1209
- const handshakeTimeout = setTimeout(() => {
1210
- if (!session) {
1211
- this.log?.warn(
1212
- `connection to ${client()} timed out waiting for handshake, closing`,
1213
- {
1214
- ...conn.loggingMetadata,
1215
- clientId: this.clientId,
1216
- connectedTo: client()
1217
- }
1218
- );
1219
- conn.telemetry?.span.setStatus({
1220
- code: import_api4.SpanStatusCode.ERROR,
1221
- message: "handshake timeout"
1222
- });
1223
- conn.close();
1224
- }
1225
- }, this.options.sessionDisconnectGraceMs);
1226
- const buffer = [];
1227
- let receivedHandshakeMessage = false;
1228
- const handshakeHandler = (data) => {
1229
- if (receivedHandshakeMessage) {
1230
- buffer.push(data);
1231
- return;
1232
- }
1233
- receivedHandshakeMessage = true;
1234
- clearTimeout(handshakeTimeout);
1235
- void this.receiveHandshakeRequestMessage(data, conn).then(
1236
- (maybeSession) => {
1237
- if (!maybeSession) {
1238
- 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);
1239
1378
  return;
1240
1379
  }
1241
- session = maybeSession;
1242
- const dataHandler = (data2) => {
1243
- const parsed = this.parseMsg(data2, conn);
1244
- if (!parsed) {
1245
- conn.close();
1246
- return;
1247
- }
1248
- this.handleMsg(parsed, conn);
1249
- };
1250
- for (const data2 of buffer) {
1251
- dataHandler(data2);
1252
- }
1253
- conn.removeDataListener(handshakeHandler);
1254
- conn.addDataListener(dataHandler);
1255
- 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);
1256
1390
  }
1257
- );
1258
- };
1259
- conn.addDataListener(handshakeHandler);
1260
- conn.addCloseListener(() => {
1261
- if (!session)
1262
- return;
1263
- this.log?.info(`connection to ${client()} disconnected`, {
1264
- ...conn.loggingMetadata,
1265
- clientId: this.clientId
1266
- });
1267
- this.onDisconnect(conn, session);
1268
- });
1269
- conn.addErrorListener((err) => {
1270
- conn.telemetry?.span.setStatus({
1271
- code: import_api4.SpanStatusCode.ERROR,
1272
- message: "connection error"
1273
- });
1274
- if (!session)
1275
- return;
1276
- this.log?.warn(
1277
- `connection to ${client()} got an error: ${coerceErrorString(err)}`,
1278
- { ...conn.loggingMetadata, clientId: this.clientId }
1279
- );
1280
- });
1281
- }
1282
- async validateHandshakeMetadata(conn, session, rawMetadata, from) {
1283
- let parsedMetadata = {};
1284
- if (this.handshakeExtensions) {
1285
- if (!import_value2.Value.Check(this.handshakeExtensions.schema, rawMetadata)) {
1286
- conn.telemetry?.span.setStatus({
1287
- code: import_api4.SpanStatusCode.ERROR,
1288
- message: "malformed handshake meta"
1289
- });
1290
- const reason = "received malformed handshake metadata";
1291
- const responseMsg = handshakeResponseMessage({
1292
- from: this.clientId,
1293
- to: from,
1294
- status: {
1295
- ok: false,
1296
- reason
1297
- }
1298
- });
1299
- conn.send(this.codec.toBuffer(responseMsg));
1300
- this.log?.warn(`received malformed handshake metadata from ${from}`, {
1301
- ...conn.loggingMetadata,
1302
- clientId: this.clientId,
1303
- validationErrors: [
1304
- ...import_value2.Value.Errors(this.handshakeExtensions.schema, rawMetadata)
1305
- ]
1306
- });
1307
- this.protocolError(ProtocolError.HandshakeFailed, reason);
1308
- return false;
1309
- }
1310
- const previousParsedMetadata = session ? this.sessionHandshakeMetadata.get(session) : void 0;
1311
- parsedMetadata = await this.handshakeExtensions.validate(
1312
- rawMetadata,
1313
- previousParsedMetadata
1314
- );
1315
- if (parsedMetadata === false) {
1316
- const reason = "rejected by handshake handler";
1317
- conn.telemetry?.span.setStatus({
1318
- code: import_api4.SpanStatusCode.ERROR,
1319
- message: reason
1320
- });
1321
- const responseMsg = handshakeResponseMessage({
1322
- from: this.clientId,
1323
- to: from,
1324
- status: {
1325
- ok: false,
1326
- reason
1327
- }
1328
- });
1329
- conn.send(this.codec.toBuffer(responseMsg));
1330
- this.log?.warn(`rejected handshake from ${from}`, {
1331
- ...conn.loggingMetadata,
1332
- clientId: this.clientId
1333
- });
1334
- this.protocolError(ProtocolError.HandshakeFailed, reason);
1335
- return false;
1336
- }
1337
- }
1338
- return parsedMetadata;
1391
+ },
1392
+ this.options,
1393
+ this.log
1394
+ );
1395
+ this.pendingSessions.add(pendingSession);
1339
1396
  }
1340
- async receiveHandshakeRequestMessage(data, conn) {
1341
- const parsed = this.parseMsg(data, conn);
1342
- if (!parsed) {
1343
- conn.telemetry?.span.setStatus({
1344
- code: import_api4.SpanStatusCode.ERROR,
1345
- message: "non-transport message"
1346
- });
1347
- this.protocolError(
1348
- ProtocolError.HandshakeFailed,
1349
- "received non-transport message"
1350
- );
1351
- return false;
1352
- }
1353
- if (!import_value2.Value.Check(ControlMessageHandshakeRequestSchema, parsed.payload)) {
1354
- conn.telemetry?.span.setStatus({
1355
- code: import_api4.SpanStatusCode.ERROR,
1356
- message: "invalid handshake request"
1357
- });
1358
- const reason = "received invalid handshake msg";
1359
- 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({
1360
1405
  from: this.clientId,
1361
- to: parsed.from,
1406
+ to,
1362
1407
  status: {
1363
1408
  ok: false,
1409
+ code,
1364
1410
  reason
1365
1411
  }
1366
- });
1367
- conn.send(this.codec.toBuffer(responseMsg2));
1368
- this.log?.warn(reason, {
1369
- ...conn.loggingMetadata,
1370
- clientId: this.clientId,
1371
- // safe to this.log metadata here as we remove the payload
1372
- // before passing it to user-land
1373
- transportMessage: parsed,
1374
- validationErrors: [
1375
- ...import_value2.Value.Errors(ControlMessageHandshakeRequestSchema, parsed.payload)
1376
- ]
1377
- });
1378
- this.protocolError(
1379
- ProtocolError.HandshakeFailed,
1380
- "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
+ }
1381
1432
  );
1382
- return false;
1433
+ return;
1383
1434
  }
1384
- const gotVersion = parsed.payload.protocolVersion;
1435
+ const gotVersion = msg.payload.protocolVersion;
1385
1436
  if (gotVersion !== PROTOCOL_VERSION) {
1386
- conn.telemetry?.span.setStatus({
1387
- code: import_api4.SpanStatusCode.ERROR,
1388
- message: "incorrect protocol version"
1389
- });
1390
- const reason = `incorrect version (got: ${gotVersion} wanted ${PROTOCOL_VERSION})`;
1391
- const responseMsg2 = handshakeResponseMessage({
1392
- from: this.clientId,
1393
- to: parsed.from,
1394
- status: {
1395
- ok: false,
1396
- 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
1397
1446
  }
1398
- });
1399
- conn.send(this.codec.toBuffer(responseMsg2));
1400
- this.log?.warn(
1401
- `received handshake msg with incompatible protocol version (got: ${gotVersion}, expected: ${PROTOCOL_VERSION})`,
1402
- { ...conn.loggingMetadata, clientId: this.clientId }
1403
1447
  );
1404
- this.protocolError(ProtocolError.HandshakeFailed, reason);
1405
- return false;
1448
+ return;
1406
1449
  }
1407
- const oldSession = this.sessions.get(parsed.from);
1450
+ let oldSession = this.sessions.get(msg.from);
1408
1451
  const parsedMetadata = await this.validateHandshakeMetadata(
1409
- conn,
1452
+ session,
1410
1453
  oldSession,
1411
- parsed.payload.metadata,
1412
- parsed.from
1454
+ msg.payload.metadata,
1455
+ msg.from
1413
1456
  );
1414
1457
  if (parsedMetadata === false) {
1415
- return false;
1458
+ return;
1416
1459
  }
1417
- let session;
1418
- let isTransparentReconnect;
1419
- if (!parsed.payload.expectedSessionState) {
1420
- ({ session, isTransparentReconnect } = this.getOrCreateSession({
1421
- to: parsed.from,
1422
- conn,
1423
- sessionId: parsed.payload.sessionId,
1424
- propagationCtx: parsed.tracing
1425
- }));
1426
- } else if (parsed.payload.expectedSessionState.reconnect) {
1427
- const existingSession = this.getExistingSession({
1428
- to: parsed.from,
1429
- sessionId: parsed.payload.sessionId,
1430
- nextExpectedSeq: parsed.payload.expectedSessionState.nextExpectedSeq
1431
- });
1432
- if (existingSession === false) {
1433
- conn.telemetry?.span.setStatus({
1434
- code: import_api4.SpanStatusCode.ERROR,
1435
- 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
+ }
1436
1500
  });
1437
- const reason = SESSION_STATE_MISMATCH;
1438
- const responseMsg2 = handshakeResponseMessage({
1439
- from: this.clientId,
1440
- to: parsed.from,
1441
- status: {
1442
- ok: false,
1443
- reason
1501
+ oldSession = noConnectionSession;
1502
+ } else if (oldSession.state === "Handshaking" /* Handshaking */) {
1503
+ const noConnectionSession = SessionStateGraph.transition.HandshakingToNoConnection(oldSession, {
1504
+ onSessionGracePeriodElapsed: () => {
1505
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1444
1506
  }
1445
1507
  });
1446
- conn.send(this.codec.toBuffer(responseMsg2));
1447
- this.log?.warn(
1448
- `'received handshake msg with incompatible existing session state: ${parsed.payload.sessionId}`,
1449
- { ...conn.loggingMetadata, clientId: this.clientId }
1450
- );
1451
- this.protocolError(ProtocolError.HandshakeFailed, reason);
1452
- 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;
1453
1516
  }
1454
- session = existingSession;
1455
- isTransparentReconnect = false;
1517
+ this.updateSession(oldSession);
1518
+ } else if (oldSession) {
1519
+ connectCase = "hard reconnection";
1520
+ this.deleteSession(oldSession);
1521
+ oldSession = void 0;
1456
1522
  } else {
1457
- const createdSession = this.createNewSession({
1458
- to: parsed.from,
1459
- conn,
1460
- sessionId: parsed.payload.sessionId,
1461
- propagationCtx: parsed.tracing
1462
- });
1463
- session = createdSession;
1464
- 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
+ }
1465
1540
  }
1466
- this.sessionHandshakeMetadata.set(session, parsedMetadata);
1467
- this.log?.debug(
1468
- `handshake from ${parsed.from} ok, responding with handshake success`,
1469
- 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
+ }
1470
1548
  );
1471
1549
  const responseMsg = handshakeResponseMessage({
1472
1550
  from: this.clientId,
1473
- to: parsed.from,
1551
+ to: msg.from,
1474
1552
  status: {
1475
1553
  ok: true,
1476
- sessionId: session.id
1554
+ sessionId
1477
1555
  }
1478
1556
  });
1479
- conn.send(this.codec.toBuffer(responseMsg));
1480
- this.onConnect(conn, session, isTransparentReconnect);
1481
- 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;
1482
1632
  }
1483
1633
  };
1484
1634