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

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