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

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