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

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