@replit/river 0.200.0-rc.1 → 0.200.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +21 -20
  2. package/dist/{chunk-XLXRFSRB.js → chunk-3CCOX55A.js} +76 -42
  3. package/dist/chunk-3CCOX55A.js.map +1 -0
  4. package/dist/chunk-GWYJZFCW.js +653 -0
  5. package/dist/chunk-GWYJZFCW.js.map +1 -0
  6. package/dist/chunk-O4O2E6DJ.js +277 -0
  7. package/dist/chunk-O4O2E6DJ.js.map +1 -0
  8. package/dist/chunk-QXLXDJD5.js +382 -0
  9. package/dist/chunk-QXLXDJD5.js.map +1 -0
  10. package/dist/{chunk-WQVOMUNR.js → chunk-UFMDEG44.js} +24 -18
  11. package/dist/chunk-UFMDEG44.js.map +1 -0
  12. package/dist/{chunk-45HIR2AS.js → chunk-UIYGPURD.js} +90 -532
  13. package/dist/chunk-UIYGPURD.js.map +1 -0
  14. package/dist/{chunk-QMM35C3H.js → chunk-VXYHC666.js} +1 -1
  15. package/dist/chunk-VXYHC666.js.map +1 -0
  16. package/dist/chunk-WSCAA7VY.js +50 -0
  17. package/dist/chunk-WSCAA7VY.js.map +1 -0
  18. package/dist/chunk-ZQVKFLAB.js +399 -0
  19. package/dist/chunk-ZQVKFLAB.js.map +1 -0
  20. package/dist/client-a84783be.d.ts +49 -0
  21. package/dist/{connection-c6521735.d.ts → connection-320fb130.d.ts} +1 -5
  22. package/dist/connection-d0b488e6.d.ts +11 -0
  23. package/dist/context-c9668e95.d.ts +527 -0
  24. package/dist/logging/index.cjs.map +1 -1
  25. package/dist/logging/index.d.cts +1 -1
  26. package/dist/logging/index.d.ts +1 -1
  27. package/dist/logging/index.js +1 -1
  28. package/dist/{index-10ebd26a.d.ts → message-fd349b27.d.ts} +31 -31
  29. package/dist/router/index.cjs +142 -544
  30. package/dist/router/index.cjs.map +1 -1
  31. package/dist/router/index.d.cts +11 -46
  32. package/dist/router/index.d.ts +11 -46
  33. package/dist/router/index.js +2 -4
  34. package/dist/server-dbad597e.d.ts +42 -0
  35. package/dist/{services-34d97070.d.ts → services-690e5553.d.ts} +11 -602
  36. package/dist/transport/impls/uds/client.cjs +1240 -1237
  37. package/dist/transport/impls/uds/client.cjs.map +1 -1
  38. package/dist/transport/impls/uds/client.d.cts +4 -3
  39. package/dist/transport/impls/uds/client.d.ts +4 -3
  40. package/dist/transport/impls/uds/client.js +7 -13
  41. package/dist/transport/impls/uds/client.js.map +1 -1
  42. package/dist/transport/impls/uds/server.cjs +1302 -1168
  43. package/dist/transport/impls/uds/server.cjs.map +1 -1
  44. package/dist/transport/impls/uds/server.d.cts +4 -4
  45. package/dist/transport/impls/uds/server.d.ts +4 -4
  46. package/dist/transport/impls/uds/server.js +6 -6
  47. package/dist/transport/impls/ws/client.cjs +981 -986
  48. package/dist/transport/impls/ws/client.cjs.map +1 -1
  49. package/dist/transport/impls/ws/client.d.cts +6 -5
  50. package/dist/transport/impls/ws/client.d.ts +6 -5
  51. package/dist/transport/impls/ws/client.js +6 -7
  52. package/dist/transport/impls/ws/client.js.map +1 -1
  53. package/dist/transport/impls/ws/server.cjs +1183 -1064
  54. package/dist/transport/impls/ws/server.cjs.map +1 -1
  55. package/dist/transport/impls/ws/server.d.cts +4 -4
  56. package/dist/transport/impls/ws/server.d.ts +4 -4
  57. package/dist/transport/impls/ws/server.js +6 -6
  58. package/dist/transport/index.cjs +1435 -1377
  59. package/dist/transport/index.cjs.map +1 -1
  60. package/dist/transport/index.d.cts +4 -26
  61. package/dist/transport/index.d.ts +4 -26
  62. package/dist/transport/index.js +9 -9
  63. package/dist/util/testHelpers.cjs +782 -333
  64. package/dist/util/testHelpers.cjs.map +1 -1
  65. package/dist/util/testHelpers.d.cts +9 -4
  66. package/dist/util/testHelpers.d.ts +9 -4
  67. package/dist/util/testHelpers.js +45 -23
  68. package/dist/util/testHelpers.js.map +1 -1
  69. package/package.json +1 -1
  70. package/dist/chunk-45HIR2AS.js.map +0 -1
  71. package/dist/chunk-75B5D7MR.js +0 -347
  72. package/dist/chunk-75B5D7MR.js.map +0 -1
  73. package/dist/chunk-AJBZUHSI.js +0 -492
  74. package/dist/chunk-AJBZUHSI.js.map +0 -1
  75. package/dist/chunk-EMIERCU7.js +0 -59
  76. package/dist/chunk-EMIERCU7.js.map +0 -1
  77. package/dist/chunk-LH2VHMQM.js +0 -335
  78. package/dist/chunk-LH2VHMQM.js.map +0 -1
  79. package/dist/chunk-QMM35C3H.js.map +0 -1
  80. package/dist/chunk-W75HU4F6.js +0 -476
  81. package/dist/chunk-W75HU4F6.js.map +0 -1
  82. package/dist/chunk-WQVOMUNR.js.map +0 -1
  83. package/dist/chunk-XLXRFSRB.js.map +0 -1
  84. package/dist/connection-0638316b.d.ts +0 -17
@@ -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 PROTOCOL_VERSION = "v1.1";
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,29 @@ 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
+ // TODO: remove optional once we know all servers
79
+ // are nextSentSeq here
80
+ // what the server expects the client to send next
81
+ nextSentSeq: import_typebox.Type.Optional(import_typebox.Type.Integer())
82
+ }),
77
83
  metadata: import_typebox.Type.Optional(import_typebox.Type.Unknown())
78
84
  });
85
+ var HandshakeErrorRetriableResponseCodes = import_typebox.Type.Union([
86
+ import_typebox.Type.Literal("SESSION_STATE_MISMATCH")
87
+ ]);
88
+ var HandshakeErrorFatalResponseCodes = import_typebox.Type.Union([
89
+ import_typebox.Type.Literal("MALFORMED_HANDSHAKE_META"),
90
+ import_typebox.Type.Literal("MALFORMED_HANDSHAKE"),
91
+ import_typebox.Type.Literal("PROTOCOL_VERSION_MISMATCH"),
92
+ import_typebox.Type.Literal("REJECTED_BY_CUSTOM_HANDLER")
93
+ ]);
94
+ var HandshakeErrorResponseCodes = import_typebox.Type.Union([
95
+ HandshakeErrorRetriableResponseCodes,
96
+ HandshakeErrorFatalResponseCodes
97
+ ]);
79
98
  var ControlMessageHandshakeResponseSchema = import_typebox.Type.Object({
80
99
  type: import_typebox.Type.Literal("HANDSHAKE_RESP"),
81
100
  status: import_typebox.Type.Union([
@@ -85,7 +104,10 @@ var ControlMessageHandshakeResponseSchema = import_typebox.Type.Object({
85
104
  }),
86
105
  import_typebox.Type.Object({
87
106
  ok: import_typebox.Type.Literal(false),
88
- reason: import_typebox.Type.String()
107
+ reason: import_typebox.Type.String(),
108
+ // TODO: remove optional once we know all servers
109
+ // are sending code here
110
+ code: import_typebox.Type.Optional(HandshakeErrorResponseCodes)
89
111
  })
90
112
  ])
91
113
  });
@@ -107,12 +129,12 @@ function handshakeRequestMessage({
107
129
  tracing
108
130
  }) {
109
131
  return {
110
- id: (0, import_nanoid.nanoid)(),
132
+ id: generateId(),
111
133
  from,
112
134
  to,
113
135
  seq: 0,
114
136
  ack: 0,
115
- streamId: (0, import_nanoid.nanoid)(),
137
+ streamId: generateId(),
116
138
  controlFlags: 0,
117
139
  tracing,
118
140
  payload: {
@@ -124,7 +146,6 @@ function handshakeRequestMessage({
124
146
  }
125
147
  };
126
148
  }
127
- var SESSION_STATE_MISMATCH = "session state mismatch";
128
149
  function isAck(controlFlag) {
129
150
  return (controlFlag & 1 /* AckBit */) === 1 /* AckBit */;
130
151
  }
@@ -186,6 +207,8 @@ var defaultTransportOptions = {
186
207
  heartbeatIntervalMs: 1e3,
187
208
  heartbeatsUntilDead: 2,
188
209
  sessionDisconnectGraceMs: 5e3,
210
+ connectionTimeoutMs: 2e3,
211
+ handshakeTimeoutMs: 1e3,
189
212
  codec: NaiveJsonCodec
190
213
  };
191
214
  var defaultConnectionRetryOptions = {
@@ -274,9 +297,6 @@ var LeakyBucketRateLimit = class {
274
297
  }
275
298
  };
276
299
 
277
- // transport/transport.ts
278
- var import_value = require("@sinclair/typebox/value");
279
-
280
300
  // logging/log.ts
281
301
  var LoggingLevels = {
282
302
  debug: -1,
@@ -367,14 +387,222 @@ var EventDispatcher = class {
367
387
  }
368
388
  };
369
389
 
370
- // transport/session.ts
371
- var import_nanoid2 = require("nanoid");
390
+ // transport/sessionStateMachine/common.ts
391
+ var import_value = require("@sinclair/typebox/value");
392
+ var ERR_CONSUMED = `session state has been consumed and is no longer valid`;
393
+ var StateMachineState = class {
394
+ /*
395
+ * Whether this state has been consumed
396
+ * and we've moved on to another state
397
+ */
398
+ _isConsumed;
399
+ close() {
400
+ this._handleClose();
401
+ }
402
+ constructor() {
403
+ this._isConsumed = false;
404
+ return new Proxy(this, {
405
+ get(target, prop) {
406
+ if (prop === "_isConsumed" || prop === "id" || prop === "state") {
407
+ return Reflect.get(target, prop);
408
+ }
409
+ if (prop === "_handleStateExit") {
410
+ return () => {
411
+ target._isConsumed = true;
412
+ target._handleStateExit();
413
+ };
414
+ }
415
+ if (prop === "_handleClose") {
416
+ return () => {
417
+ target._handleStateExit();
418
+ target._handleClose();
419
+ };
420
+ }
421
+ if (target._isConsumed) {
422
+ throw new Error(
423
+ `${ERR_CONSUMED}: getting ${prop.toString()} on consumed state`
424
+ );
425
+ }
426
+ return Reflect.get(target, prop);
427
+ },
428
+ set(target, prop, value) {
429
+ if (target._isConsumed) {
430
+ throw new Error(
431
+ `${ERR_CONSUMED}: setting ${prop.toString()} on consumed state`
432
+ );
433
+ }
434
+ return Reflect.set(target, prop, value);
435
+ }
436
+ });
437
+ }
438
+ };
439
+ var CommonSession = class extends StateMachineState {
440
+ from;
441
+ options;
442
+ log;
443
+ constructor(from, options, log) {
444
+ super();
445
+ this.from = from;
446
+ this.options = options;
447
+ this.log = log;
448
+ }
449
+ parseMsg(msg) {
450
+ const parsedMsg = this.options.codec.fromBuffer(msg);
451
+ if (parsedMsg === null) {
452
+ const decodedBuffer = new TextDecoder().decode(Buffer.from(msg));
453
+ this.log?.error(
454
+ `received malformed msg: ${decodedBuffer}`,
455
+ this.loggingMetadata
456
+ );
457
+ return null;
458
+ }
459
+ if (!import_value.Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
460
+ this.log?.error(`received invalid msg: ${JSON.stringify(parsedMsg)}`, {
461
+ ...this.loggingMetadata,
462
+ validationErrors: [
463
+ ...import_value.Value.Errors(OpaqueTransportMessageSchema, parsedMsg)
464
+ ]
465
+ });
466
+ return null;
467
+ }
468
+ return parsedMsg;
469
+ }
470
+ };
471
+ var IdentifiedSession = class extends CommonSession {
472
+ id;
473
+ telemetry;
474
+ to;
475
+ /**
476
+ * Index of the message we will send next (excluding handshake)
477
+ */
478
+ seq;
479
+ /**
480
+ * Number of unique messages we've received this session (excluding handshake)
481
+ */
482
+ ack;
483
+ sendBuffer;
484
+ constructor(id, from, to, seq, ack, sendBuffer, telemetry, options, log) {
485
+ super(from, options, log);
486
+ this.id = id;
487
+ this.to = to;
488
+ this.seq = seq;
489
+ this.ack = ack;
490
+ this.sendBuffer = sendBuffer;
491
+ this.telemetry = telemetry;
492
+ this.log = log;
493
+ }
494
+ get loggingMetadata() {
495
+ const spanContext = this.telemetry.span.spanContext();
496
+ return {
497
+ clientId: this.from,
498
+ connectedTo: this.to,
499
+ sessionId: this.id,
500
+ telemetry: {
501
+ traceId: spanContext.traceId,
502
+ spanId: spanContext.spanId
503
+ }
504
+ };
505
+ }
506
+ constructMsg(partialMsg) {
507
+ const msg = {
508
+ ...partialMsg,
509
+ id: generateId(),
510
+ to: this.to,
511
+ from: this.from,
512
+ seq: this.seq,
513
+ ack: this.ack
514
+ };
515
+ this.seq++;
516
+ return msg;
517
+ }
518
+ nextSeq() {
519
+ return this.sendBuffer.length > 0 ? this.sendBuffer[0].seq : this.seq;
520
+ }
521
+ send(msg) {
522
+ const constructedMsg = this.constructMsg(msg);
523
+ this.sendBuffer.push(constructedMsg);
524
+ return constructedMsg.id;
525
+ }
526
+ _handleStateExit() {
527
+ }
528
+ _handleClose() {
529
+ this.sendBuffer.length = 0;
530
+ this.telemetry.span.end();
531
+ }
532
+ };
533
+
534
+ // transport/sessionStateMachine/SessionConnecting.ts
535
+ var SessionConnecting = class extends IdentifiedSession {
536
+ state = "Connecting" /* Connecting */;
537
+ connPromise;
538
+ listeners;
539
+ connectionTimeout;
540
+ constructor(connPromise, listeners, ...args) {
541
+ super(...args);
542
+ this.connPromise = connPromise;
543
+ this.listeners = listeners;
544
+ this.connectionTimeout = setTimeout(() => {
545
+ listeners.onConnectionTimeout();
546
+ }, this.options.connectionTimeoutMs);
547
+ connPromise.then(
548
+ (conn) => {
549
+ if (this._isConsumed)
550
+ return;
551
+ listeners.onConnectionEstablished(conn);
552
+ },
553
+ (err) => {
554
+ if (this._isConsumed)
555
+ return;
556
+ listeners.onConnectionFailed(err);
557
+ }
558
+ );
559
+ }
560
+ // close a pending connection if it resolves, ignore errors if the promise
561
+ // ends up rejected anyways
562
+ bestEffortClose() {
563
+ void this.connPromise.then((conn) => conn.close()).catch(() => {
564
+ });
565
+ }
566
+ _handleStateExit() {
567
+ super._handleStateExit();
568
+ clearTimeout(this.connectionTimeout);
569
+ this.connectionTimeout = void 0;
570
+ }
571
+ _handleClose() {
572
+ this.bestEffortClose();
573
+ super._handleClose();
574
+ }
575
+ };
576
+
577
+ // transport/sessionStateMachine/SessionNoConnection.ts
578
+ var SessionNoConnection = class extends IdentifiedSession {
579
+ state = "NoConnection" /* NoConnection */;
580
+ listeners;
581
+ gracePeriodTimeout;
582
+ constructor(listeners, ...args) {
583
+ super(...args);
584
+ this.listeners = listeners;
585
+ this.gracePeriodTimeout = setTimeout(() => {
586
+ this.listeners.onSessionGracePeriodElapsed();
587
+ }, this.options.sessionDisconnectGraceMs);
588
+ }
589
+ _handleClose() {
590
+ super._handleClose();
591
+ }
592
+ _handleStateExit() {
593
+ super._handleStateExit();
594
+ if (this.gracePeriodTimeout) {
595
+ clearTimeout(this.gracePeriodTimeout);
596
+ this.gracePeriodTimeout = void 0;
597
+ }
598
+ }
599
+ };
372
600
 
373
601
  // tracing/index.ts
374
602
  var import_api = require("@opentelemetry/api");
375
603
 
376
604
  // package.json
377
- var version = "0.200.0-rc.1";
605
+ var version = "0.200.0-rc.3";
378
606
 
379
607
  // tracing/index.ts
380
608
  function getPropagationContext(ctx) {
@@ -385,16 +613,16 @@ function getPropagationContext(ctx) {
385
613
  import_api.propagation.inject(ctx, tracing);
386
614
  return tracing;
387
615
  }
388
- function createSessionTelemetryInfo(session, propagationCtx) {
616
+ function createSessionTelemetryInfo(sessionId, to, from, propagationCtx) {
389
617
  const parentCtx = propagationCtx ? import_api.propagation.extract(import_api.context.active(), propagationCtx) : import_api.context.active();
390
618
  const span = tracer.startSpan(
391
- `session ${session.id}`,
619
+ `session ${sessionId}`,
392
620
  {
393
621
  attributes: {
394
622
  component: "river",
395
- "river.session.id": session.id,
396
- "river.session.to": session.to,
397
- "river.session.from": session.from
623
+ "river.session.id": sessionId,
624
+ "river.session.to": to,
625
+ "river.session.from": from
398
626
  }
399
627
  },
400
628
  parentCtx
@@ -402,173 +630,155 @@ function createSessionTelemetryInfo(session, propagationCtx) {
402
630
  const ctx = import_api.trace.setSpan(parentCtx, span);
403
631
  return { span, ctx };
404
632
  }
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
633
  var tracer = import_api.trace.getTracer("river", version);
421
634
  var tracing_default = tracer;
422
635
 
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)}`;
432
- }
433
- 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
- };
636
+ // transport/sessionStateMachine/SessionWaitingForHandshake.ts
637
+ var SessionWaitingForHandshake = class extends CommonSession {
638
+ state = "WaitingForHandshake" /* WaitingForHandshake */;
639
+ conn;
640
+ listeners;
641
+ handshakeTimeout;
642
+ constructor(conn, listeners, ...args) {
643
+ super(...args);
644
+ this.conn = conn;
645
+ this.listeners = listeners;
646
+ this.handshakeTimeout = setTimeout(() => {
647
+ listeners.onHandshakeTimeout();
648
+ }, this.options.handshakeTimeoutMs);
649
+ this.conn.addDataListener(this.onHandshakeData);
650
+ this.conn.addErrorListener(listeners.onConnectionErrored);
651
+ this.conn.addCloseListener(listeners.onConnectionClosed);
652
+ }
653
+ onHandshakeData = (msg) => {
654
+ const parsedMsg = this.parseMsg(msg);
655
+ if (parsedMsg === null) {
656
+ this.listeners.onInvalidHandshake("could not parse message");
657
+ return;
441
658
  }
442
- return metadata;
443
- }
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);
508
- }
509
- bindLogger(log) {
510
- this.log = log;
511
- }
659
+ this.listeners.onHandshake(parsedMsg);
660
+ };
512
661
  get loggingMetadata() {
513
- const spanContext = this.telemetry.span.spanContext();
514
662
  return {
515
663
  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
- }
664
+ connId: this.conn.id
523
665
  };
524
666
  }
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
- */
667
+ sendHandshake(msg) {
668
+ return this.conn.send(this.options.codec.toBuffer(msg));
669
+ }
670
+ _handleStateExit() {
671
+ this.conn.removeDataListener(this.onHandshakeData);
672
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
673
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
674
+ clearTimeout(this.handshakeTimeout);
675
+ this.handshakeTimeout = void 0;
676
+ }
677
+ _handleClose() {
678
+ this.conn.close();
679
+ }
680
+ };
681
+
682
+ // transport/sessionStateMachine/SessionHandshaking.ts
683
+ var SessionHandshaking = class extends IdentifiedSession {
684
+ state = "Handshaking" /* Handshaking */;
685
+ conn;
686
+ listeners;
687
+ handshakeTimeout;
688
+ constructor(conn, listeners, ...args) {
689
+ super(...args);
690
+ this.conn = conn;
691
+ this.listeners = listeners;
692
+ this.handshakeTimeout = setTimeout(() => {
693
+ listeners.onHandshakeTimeout();
694
+ }, this.options.handshakeTimeoutMs);
695
+ this.conn.addDataListener(this.onHandshakeData);
696
+ this.conn.addErrorListener(listeners.onConnectionErrored);
697
+ this.conn.addCloseListener(listeners.onConnectionClosed);
698
+ }
699
+ onHandshakeData = (msg) => {
700
+ const parsedMsg = this.parseMsg(msg);
701
+ if (parsedMsg === null) {
702
+ this.listeners.onInvalidHandshake("could not parse message");
703
+ return;
704
+ }
705
+ this.listeners.onHandshake(parsedMsg);
706
+ };
707
+ sendHandshake(msg) {
708
+ return this.conn.send(this.options.codec.toBuffer(msg));
709
+ }
710
+ _handleStateExit() {
711
+ super._handleStateExit();
712
+ this.conn.removeDataListener(this.onHandshakeData);
713
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
714
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
715
+ clearTimeout(this.handshakeTimeout);
716
+ }
717
+ _handleClose() {
718
+ super._handleClose();
719
+ this.conn.close();
720
+ }
721
+ };
722
+
723
+ // transport/sessionStateMachine/SessionConnected.ts
724
+ var import_api2 = require("@opentelemetry/api");
725
+ var SessionConnected = class extends IdentifiedSession {
726
+ state = "Connected" /* Connected */;
727
+ conn;
728
+ listeners;
729
+ heartbeatHandle;
730
+ heartbeatMisses = 0;
731
+ get isActivelyHeartbeating() {
732
+ return this.heartbeatHandle !== void 0;
733
+ }
734
+ updateBookkeeping(ack, seq) {
735
+ this.sendBuffer = this.sendBuffer.filter((unacked) => unacked.seq >= ack);
736
+ this.ack = seq + 1;
737
+ this.heartbeatMisses = 0;
738
+ }
533
739
  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;
543
- 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 {
740
+ const constructedMsg = this.constructMsg(msg);
741
+ this.sendBuffer.push(constructedMsg);
742
+ this.conn.send(this.options.codec.toBuffer(constructedMsg));
743
+ return constructedMsg.id;
744
+ }
745
+ constructor(conn, listeners, ...args) {
746
+ super(...args);
747
+ this.conn = conn;
748
+ this.listeners = listeners;
749
+ this.conn.addDataListener(this.onMessageData);
750
+ this.conn.addCloseListener(listeners.onConnectionClosed);
751
+ this.conn.addErrorListener(listeners.onConnectionErrored);
752
+ if (this.sendBuffer.length > 0) {
551
753
  this.log?.debug(
552
- `buffering msg to ${fullMsg.to}, connection not ready yet`,
553
- { ...this.loggingMetadata, transportMessage: fullMsg }
754
+ `sending ${this.sendBuffer.length} buffered messages`,
755
+ this.loggingMetadata
554
756
  );
555
757
  }
556
- return fullMsg.id;
758
+ for (const msg of this.sendBuffer) {
759
+ conn.send(this.options.codec.toBuffer(msg));
760
+ }
557
761
  }
558
- sendHeartbeat() {
559
- const misses = this.heartbeatMisses;
560
- const missDuration = misses * this.options.heartbeatIntervalMs;
561
- if (misses > this.options.heartbeatsUntilDead) {
562
- if (this.connection) {
762
+ startActiveHeartbeat() {
763
+ this.heartbeatHandle = setInterval(() => {
764
+ const misses = this.heartbeatMisses;
765
+ const missDuration = misses * this.options.heartbeatIntervalMs;
766
+ if (misses >= this.options.heartbeatsUntilDead) {
563
767
  this.log?.info(
564
768
  `closing connection to ${this.to} due to inactivity (missed ${misses} heartbeats which is ${missDuration}ms)`,
565
769
  this.loggingMetadata
566
770
  );
567
771
  this.telemetry.span.addEvent("closing connection due to inactivity");
568
- this.closeStaleConnection();
772
+ this.conn.close();
773
+ clearInterval(this.heartbeatHandle);
774
+ this.heartbeatHandle = void 0;
775
+ return;
569
776
  }
570
- return;
571
- }
777
+ this.sendHeartbeat();
778
+ this.heartbeatMisses++;
779
+ }, this.options.heartbeatIntervalMs);
780
+ }
781
+ sendHeartbeat() {
572
782
  this.send({
573
783
  streamId: "heartbeat",
574
784
  controlFlags: 1 /* AckBit */,
@@ -576,186 +786,255 @@ var Session = class {
576
786
  type: "ACK"
577
787
  }
578
788
  });
579
- this.heartbeatMisses++;
580
- }
581
- resetBufferedMessages() {
582
- this.sendBuffer = [];
583
- this.seq = 0;
584
- this.ack = 0;
585
789
  }
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, {
790
+ onMessageData = (msg) => {
791
+ const parsedMsg = this.parseMsg(msg);
792
+ if (parsedMsg === null)
793
+ return;
794
+ if (parsedMsg.seq !== this.ack) {
795
+ if (parsedMsg.seq < this.ack) {
796
+ this.log?.debug(
797
+ `received duplicate msg (got seq: ${parsedMsg.seq}, wanted seq: ${this.ack}), discarding`,
798
+ {
799
+ ...this.loggingMetadata,
800
+ transportMessage: parsedMsg
801
+ }
802
+ );
803
+ } else {
804
+ const reason = `received out-of-order msg (got seq: ${parsedMsg.seq}, wanted seq: ${this.ack})`;
805
+ this.log?.error(reason, {
605
806
  ...this.loggingMetadata,
606
- transportMessage: msg,
607
- connId: conn.id,
807
+ transportMessage: parsedMsg,
608
808
  tags: ["invariant-violation"]
609
809
  });
610
- conn.close();
611
- return;
810
+ this.telemetry.span.setStatus({
811
+ code: import_api2.SpanStatusCode.ERROR,
812
+ message: reason
813
+ });
814
+ this.listeners.onInvalidMessage(reason);
612
815
  }
613
- }
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
- });
621
816
  return;
622
817
  }
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)
818
+ this.log?.debug(`received msg`, {
819
+ ...this.loggingMetadata,
820
+ transportMessage: parsedMsg
821
+ });
822
+ this.updateBookkeeping(parsedMsg.ack, parsedMsg.seq);
823
+ if (!isAck(parsedMsg.controlFlags)) {
824
+ this.listeners.onMessage(parsedMsg);
628
825
  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);
641
826
  }
642
- this.connection = newConn;
643
- this.handshakingConnection = void 0;
644
- }
645
- replaceWithNewHandshakingConnection(newConn) {
646
- this.handshakingConnection = newConn;
827
+ this.log?.debug(`discarding msg (ack bit set)`, {
828
+ ...this.loggingMetadata,
829
+ transportMessage: parsedMsg
830
+ });
831
+ if (!this.isActivelyHeartbeating) {
832
+ this.sendHeartbeat();
833
+ }
834
+ };
835
+ _handleStateExit() {
836
+ super._handleStateExit();
837
+ this.conn.removeDataListener(this.onMessageData);
838
+ this.conn.removeCloseListener(this.listeners.onConnectionClosed);
839
+ this.conn.removeErrorListener(this.listeners.onConnectionErrored);
840
+ clearInterval(this.heartbeatHandle);
841
+ this.heartbeatHandle = void 0;
842
+ }
843
+ _handleClose() {
844
+ super._handleClose();
845
+ this.conn.close();
647
846
  }
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
847
+ };
848
+
849
+ // transport/sessionStateMachine/transitions.ts
850
+ function inheritSharedSession(session) {
851
+ return [
852
+ session.id,
853
+ session.from,
854
+ session.to,
855
+ session.seq,
856
+ session.ack,
857
+ session.sendBuffer,
858
+ session.telemetry,
859
+ session.options,
860
+ session.log
861
+ ];
862
+ }
863
+ var SessionStateGraph = {
864
+ entrypoints: {
865
+ NoConnection(to, from, listeners, options, log) {
866
+ const id = `session-${generateId()}`;
867
+ const telemetry = createSessionTelemetryInfo(id, to, from);
868
+ const sendBuffer = [];
869
+ const session = new SessionNoConnection(
870
+ listeners,
871
+ id,
872
+ from,
873
+ to,
874
+ 0,
875
+ 0,
876
+ sendBuffer,
877
+ telemetry,
878
+ options,
879
+ log
658
880
  );
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;
881
+ session.log?.info(`session ${session.id} created in NoConnection state`, {
882
+ ...session.loggingMetadata,
883
+ tags: ["state-transition"]
884
+ });
885
+ return session;
886
+ },
887
+ WaitingForHandshake(from, conn, listeners, options, log) {
888
+ const session = new SessionWaitingForHandshake(
889
+ conn,
890
+ listeners,
891
+ from,
892
+ options,
893
+ log
894
+ );
895
+ session.log?.info(`session created in WaitingForHandshake state`, {
896
+ ...session.loggingMetadata,
897
+ tags: ["state-transition"]
898
+ });
899
+ return session;
676
900
  }
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
- }
694
- get nextExpectedSeq() {
695
- return this.ack;
696
- }
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
- }
901
+ },
902
+ // All of the transitions 'move'/'consume' the old session and return a new one.
903
+ // After a session is transitioned, any usage of the old session will throw.
904
+ transition: {
905
+ // happy path transitions
906
+ NoConnectionToConnecting(oldSession, connPromise, listeners) {
907
+ const carriedState = inheritSharedSession(oldSession);
908
+ oldSession._handleStateExit();
909
+ const session = new SessionConnecting(
910
+ connPromise,
911
+ listeners,
912
+ ...carriedState
913
+ );
914
+ session.log?.info(
915
+ `session ${session.id} transition from NoConnection to Connecting`,
916
+ {
917
+ ...session.loggingMetadata,
918
+ tags: ["state-transition"]
919
+ }
920
+ );
921
+ return session;
922
+ },
923
+ ConnectingToHandshaking(oldSession, conn, listeners) {
924
+ const carriedState = inheritSharedSession(oldSession);
925
+ oldSession._handleStateExit();
926
+ const session = new SessionHandshaking(conn, listeners, ...carriedState);
927
+ session.log?.info(
928
+ `session ${session.id} transition from Connecting to Handshaking`,
929
+ {
930
+ ...session.loggingMetadata,
931
+ tags: ["state-transition"]
932
+ }
933
+ );
934
+ return session;
935
+ },
936
+ HandshakingToConnected(oldSession, listeners) {
937
+ const carriedState = inheritSharedSession(oldSession);
938
+ const conn = oldSession.conn;
939
+ oldSession._handleStateExit();
940
+ const session = new SessionConnected(conn, listeners, ...carriedState);
941
+ session.log?.info(
942
+ `session ${session.id} transition from Handshaking to Connected`,
943
+ {
944
+ ...session.loggingMetadata,
945
+ tags: ["state-transition"]
946
+ }
947
+ );
948
+ return session;
949
+ },
950
+ WaitingForHandshakeToConnected(pendingSession, oldSession, sessionId, to, propagationCtx, listeners) {
951
+ const conn = pendingSession.conn;
952
+ const { from, options } = pendingSession;
953
+ const carriedState = oldSession ? (
954
+ // old session exists, inherit state
955
+ inheritSharedSession(oldSession)
956
+ ) : (
957
+ // old session does not exist, create new state
958
+ [
959
+ sessionId,
960
+ from,
961
+ to,
962
+ 0,
963
+ 0,
964
+ [],
965
+ createSessionTelemetryInfo(sessionId, to, from, propagationCtx),
966
+ options,
967
+ pendingSession.log
968
+ ]
969
+ );
970
+ pendingSession._handleStateExit();
971
+ oldSession?._handleStateExit();
972
+ const session = new SessionConnected(conn, listeners, ...carriedState);
973
+ session.log?.info(
974
+ `session ${session.id} transition from WaitingForHandshake to Connected`,
975
+ {
976
+ ...session.loggingMetadata,
977
+ tags: ["state-transition"]
978
+ }
979
+ );
980
+ return session;
981
+ },
982
+ // disconnect paths
983
+ ConnectingToNoConnection(oldSession, listeners) {
984
+ const carriedState = inheritSharedSession(oldSession);
985
+ oldSession.bestEffortClose();
986
+ oldSession._handleStateExit();
987
+ const session = new SessionNoConnection(listeners, ...carriedState);
988
+ session.log?.info(
989
+ `session ${session.id} transition from Connecting to NoConnection`,
990
+ {
991
+ ...session.loggingMetadata,
992
+ tags: ["state-transition"]
993
+ }
994
+ );
995
+ return session;
996
+ },
997
+ HandshakingToNoConnection(oldSession, listeners) {
998
+ const carriedState = inheritSharedSession(oldSession);
999
+ oldSession.conn.close();
1000
+ oldSession._handleStateExit();
1001
+ const session = new SessionNoConnection(listeners, ...carriedState);
1002
+ session.log?.info(
1003
+ `session ${session.id} transition from Handshaking to NoConnection`,
1004
+ {
1005
+ ...session.loggingMetadata,
1006
+ tags: ["state-transition"]
1007
+ }
1008
+ );
1009
+ return session;
1010
+ },
1011
+ ConnectedToNoConnection(oldSession, listeners) {
1012
+ const carriedState = inheritSharedSession(oldSession);
1013
+ oldSession.conn.close();
1014
+ oldSession._handleStateExit();
1015
+ const session = new SessionNoConnection(listeners, ...carriedState);
1016
+ session.log?.info(
1017
+ `session ${session.id} transition from Connected to NoConnection`,
1018
+ {
1019
+ ...session.loggingMetadata,
1020
+ tags: ["state-transition"]
1021
+ }
1022
+ );
1023
+ return session;
706
1024
  }
707
- return nextExpectedSeq === this.seq;
708
- }
709
- // This is only used in tests to make the session misbehave.
710
- /* @internal */
711
- advanceAckForTesting(by) {
712
- this.ack += by;
713
- }
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;
726
- }
727
- inspectSendBuffer() {
728
- return this.sendBuffer;
729
1025
  }
730
1026
  };
731
1027
 
732
1028
  // transport/transport.ts
733
- var import_api3 = require("@opentelemetry/api");
734
1029
  var Transport = class {
735
1030
  /**
736
1031
  * The status of the transport.
737
1032
  */
738
1033
  status;
739
- /**
740
- * The {@link Codec} used to encode and decode messages.
741
- */
742
- codec;
743
1034
  /**
744
1035
  * The client ID of this transport.
745
1036
  */
746
1037
  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
1038
  /**
760
1039
  * The event dispatcher for handling events of type EventTypes.
761
1040
  */
@@ -765,19 +1044,18 @@ var Transport = class {
765
1044
  */
766
1045
  options;
767
1046
  log;
1047
+ sessions;
768
1048
  /**
769
1049
  * Creates a new Transport instance.
770
- * This should also set up {@link onConnect}, and {@link onDisconnect} listeners.
771
1050
  * @param codec The codec used to encode and decode messages.
772
1051
  * @param clientId The client ID of this transport.
773
1052
  */
774
1053
  constructor(clientId, providedOptions) {
775
1054
  this.options = { ...defaultTransportOptions, ...providedOptions };
776
1055
  this.eventDispatcher = new EventDispatcher();
777
- this.sessions = /* @__PURE__ */ new Map();
778
- this.codec = this.options.codec;
779
1056
  this.clientId = clientId;
780
1057
  this.status = "open";
1058
+ this.sessions = /* @__PURE__ */ new Map();
781
1059
  }
782
1060
  bindLogger(fn, level) {
783
1061
  if (typeof fn === "function") {
@@ -786,299 +1064,15 @@ var Transport = class {
786
1064
  }
787
1065
  this.log = createLogProxy(fn);
788
1066
  }
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
1067
  /**
1022
1068
  * Called when a message is received by this transport.
1023
1069
  * You generally shouldn't need to override this in downstream transport implementations.
1024
1070
  * @param msg The received message.
1025
1071
  */
1026
- handleMsg(msg, conn) {
1072
+ handleMsg(msg) {
1027
1073
  if (this.getStatus() !== "open")
1028
1074
  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
- }
1075
+ this.eventDispatcher.dispatchEvent("message", msg);
1082
1076
  }
1083
1077
  /**
1084
1078
  * Adds a listener to this transport.
@@ -1096,50 +1090,6 @@ var Transport = class {
1096
1090
  removeEventListener(type, handler) {
1097
1091
  this.eventDispatcher.removeEventListener(type, handler);
1098
1092
  }
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
- });
1126
- }
1127
- sendRequestCloseControl(to, streamId) {
1128
- return this.send(to, {
1129
- streamId,
1130
- controlFlags: 16 /* StreamCloseRequestBit */,
1131
- payload: {
1132
- type: "CLOSE"
1133
- }
1134
- });
1135
- }
1136
- sendAbort(to, streamId, payload) {
1137
- return this.send(to, {
1138
- streamId,
1139
- controlFlags: 4 /* StreamAbortBit */,
1140
- payload
1141
- });
1142
- }
1143
1093
  protocolError(type, message) {
1144
1094
  this.eventDispatcher.dispatchEvent("protocolError", { type, message });
1145
1095
  }
@@ -1151,7 +1101,7 @@ var Transport = class {
1151
1101
  close() {
1152
1102
  this.status = "closed";
1153
1103
  for (const session of this.sessions.values()) {
1154
- this.deleteSession({ session, closeHandshakingConnection: true });
1104
+ this.deleteSession(session);
1155
1105
  }
1156
1106
  this.eventDispatcher.dispatchEvent("transportStatus", {
1157
1107
  status: this.status
@@ -1162,6 +1112,68 @@ var Transport = class {
1162
1112
  getStatus() {
1163
1113
  return this.status;
1164
1114
  }
1115
+ updateSession(session) {
1116
+ const activeSession = this.sessions.get(session.to);
1117
+ if (activeSession && activeSession.id !== session.id) {
1118
+ const msg = `attempt to transition active session for ${session.to} but active session (${activeSession.id}) is different from handle (${session.id})`;
1119
+ throw new Error(msg);
1120
+ }
1121
+ this.sessions.set(session.to, session);
1122
+ if (!activeSession) {
1123
+ this.eventDispatcher.dispatchEvent("sessionStatus", {
1124
+ status: "connect",
1125
+ session
1126
+ });
1127
+ }
1128
+ this.eventDispatcher.dispatchEvent("sessionTransition", {
1129
+ state: session.state,
1130
+ session
1131
+ });
1132
+ return session;
1133
+ }
1134
+ // state transitions
1135
+ deleteSession(session) {
1136
+ session.log?.info(`closing session ${session.id}`, session.loggingMetadata);
1137
+ this.eventDispatcher.dispatchEvent("sessionStatus", {
1138
+ status: "disconnect",
1139
+ session
1140
+ });
1141
+ session.close();
1142
+ this.sessions.delete(session.to);
1143
+ }
1144
+ // common listeners
1145
+ onSessionGracePeriodElapsed(session) {
1146
+ this.log?.warn(
1147
+ `session to ${session.to} grace period elapsed, closing`,
1148
+ session.loggingMetadata
1149
+ );
1150
+ this.deleteSession(session);
1151
+ }
1152
+ onConnectingFailed(session) {
1153
+ const noConnectionSession = SessionStateGraph.transition.ConnectingToNoConnection(session, {
1154
+ onSessionGracePeriodElapsed: () => {
1155
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1156
+ }
1157
+ });
1158
+ return this.updateSession(noConnectionSession);
1159
+ }
1160
+ onConnClosed(session) {
1161
+ let noConnectionSession;
1162
+ if (session.state === "Handshaking" /* Handshaking */) {
1163
+ noConnectionSession = SessionStateGraph.transition.HandshakingToNoConnection(session, {
1164
+ onSessionGracePeriodElapsed: () => {
1165
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1166
+ }
1167
+ });
1168
+ } else {
1169
+ noConnectionSession = SessionStateGraph.transition.ConnectedToNoConnection(session, {
1170
+ onSessionGracePeriodElapsed: () => {
1171
+ this.onSessionGracePeriodElapsed(noConnectionSession);
1172
+ }
1173
+ });
1174
+ }
1175
+ return this.updateSession(noConnectionSession);
1176
+ }
1165
1177
  };
1166
1178
 
1167
1179
  // util/stringify.ts
@@ -1179,10 +1191,6 @@ var ClientTransport = class extends Transport {
1179
1191
  * The options for this transport.
1180
1192
  */
1181
1193
  options;
1182
- /**
1183
- * The map of reconnect promises for each client ID.
1184
- */
1185
- inflightConnectionPromises;
1186
1194
  retryBudget;
1187
1195
  /**
1188
1196
  * A flag indicating whether the transport should automatically reconnect
@@ -1201,352 +1209,278 @@ var ClientTransport = class extends Transport {
1201
1209
  ...defaultClientTransportOptions,
1202
1210
  ...providedOptions
1203
1211
  };
1204
- this.inflightConnectionPromises = /* @__PURE__ */ new Map();
1205
1212
  this.retryBudget = new LeakyBucketRateLimit(this.options);
1206
1213
  }
1207
1214
  extendHandshake(options) {
1208
1215
  this.handshakeExtensions = options;
1209
1216
  }
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);
1217
+ tryReconnecting(to) {
1218
+ if (this.reconnectOnConnectionDrop && this.getStatus() === "open") {
1219
+ this.connect(to);
1220
+ }
1221
+ }
1222
+ send(to, msg) {
1223
+ if (this.getStatus() === "closed") {
1224
+ const err = "transport is closed, cant send";
1225
+ this.log?.error(err, {
1226
+ clientId: this.clientId,
1227
+ transportMessage: msg,
1228
+ tags: ["invariant-violation"]
1244
1229
  });
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
1230
+ throw new Error(err);
1231
+ }
1232
+ let session = this.sessions.get(to);
1233
+ if (!session) {
1234
+ session = this.createUnconnectedSession(to);
1235
+ }
1236
+ return session.send(msg);
1237
+ }
1238
+ createUnconnectedSession(to) {
1239
+ const session = SessionStateGraph.entrypoints.NoConnection(
1240
+ to,
1241
+ this.clientId,
1242
+ {
1243
+ onSessionGracePeriodElapsed: () => {
1244
+ this.onSessionGracePeriodElapsed(session);
1259
1245
  }
1260
- );
1261
- this.inflightConnectionPromises.delete(to);
1262
- if (this.reconnectOnConnectionDrop) {
1263
- void this.connect(to);
1246
+ },
1247
+ this.options,
1248
+ this.log
1249
+ );
1250
+ this.updateSession(session);
1251
+ return session;
1252
+ }
1253
+ // listeners
1254
+ onConnectingFailed(session) {
1255
+ const noConnectionSession = super.onConnectingFailed(session);
1256
+ this.tryReconnecting(noConnectionSession.to);
1257
+ return noConnectionSession;
1258
+ }
1259
+ onConnClosed(session) {
1260
+ const noConnectionSession = super.onConnClosed(session);
1261
+ this.tryReconnecting(noConnectionSession.to);
1262
+ return noConnectionSession;
1263
+ }
1264
+ onConnectionEstablished(session, conn) {
1265
+ const handshakingSession = SessionStateGraph.transition.ConnectingToHandshaking(session, conn, {
1266
+ onConnectionErrored: (err) => {
1267
+ const errStr = coerceErrorString(err);
1268
+ this.log?.error(
1269
+ `connection to ${handshakingSession.to} errored during handshake: ${errStr}`,
1270
+ handshakingSession.loggingMetadata
1271
+ );
1272
+ },
1273
+ onConnectionClosed: () => {
1274
+ this.log?.warn(
1275
+ `connection to ${handshakingSession.to} closed during handshake`,
1276
+ handshakingSession.loggingMetadata
1277
+ );
1278
+ this.onConnClosed(handshakingSession);
1279
+ },
1280
+ onHandshake: (msg) => {
1281
+ this.onHandshakeResponse(handshakingSession, msg);
1282
+ },
1283
+ onInvalidHandshake: (reason) => {
1284
+ this.log?.error(
1285
+ `invalid handshake: ${reason}`,
1286
+ handshakingSession.loggingMetadata
1287
+ );
1288
+ this.deleteSession(session);
1289
+ this.protocolError(ProtocolError.HandshakeFailed, reason);
1290
+ },
1291
+ onHandshakeTimeout: () => {
1292
+ this.log?.error(
1293
+ `connection to ${handshakingSession.to} timed out during handshake`,
1294
+ handshakingSession.loggingMetadata
1295
+ );
1296
+ this.onConnClosed(handshakingSession);
1264
1297
  }
1265
1298
  });
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
1278
- }
1279
- );
1299
+ this.updateSession(handshakingSession);
1300
+ void this.sendHandshake(handshakingSession);
1301
+ return handshakingSession;
1302
+ }
1303
+ rejectHandshakeResponse(session, reason, metadata) {
1304
+ session.conn.telemetry?.span.setStatus({
1305
+ code: import_api3.SpanStatusCode.ERROR,
1306
+ message: reason
1280
1307
  });
1308
+ this.log?.warn(reason, metadata);
1309
+ this.deleteSession(session);
1281
1310
  }
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,
1311
+ onHandshakeResponse(session, msg) {
1312
+ if (!import_value2.Value.Check(ControlMessageHandshakeResponseSchema, msg.payload)) {
1313
+ const reason = `received invalid handshake response`;
1314
+ this.rejectHandshakeResponse(session, reason, {
1315
+ ...session.loggingMetadata,
1316
+ transportMessage: msg,
1305
1317
  validationErrors: [
1306
- ...import_value2.Value.Errors(
1307
- ControlMessageHandshakeResponseSchema,
1308
- parsed.payload
1309
- )
1318
+ ...import_value2.Value.Errors(ControlMessageHandshakeResponseSchema, msg.payload)
1310
1319
  ]
1311
1320
  });
1312
- this.protocolError(
1313
- ProtocolError.HandshakeFailed,
1314
- "invalid handshake resp"
1315
- );
1316
- return false;
1321
+ return;
1317
1322
  }
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
- });
1323
+ if (!msg.payload.status.ok) {
1324
+ const retriable = msg.payload.status.code ? import_value2.Value.Check(
1325
+ HandshakeErrorRetriableResponseCodes,
1326
+ msg.payload.status.code
1327
+ ) : false;
1328
+ const reason = `handshake failed: ${msg.payload.status.reason}`;
1329
+ this.rejectHandshakeResponse(session, reason, {
1330
+ ...session.loggingMetadata,
1331
+ transportMessage: msg
1332
+ });
1333
+ if (retriable) {
1334
+ this.tryReconnecting(session.to);
1331
1335
  } else {
1332
- conn.telemetry?.span.setStatus({
1333
- code: import_api4.SpanStatusCode.ERROR,
1334
- message: "handshake rejected"
1335
- });
1336
+ this.deleteSession(session);
1337
+ this.protocolError(ProtocolError.HandshakeFailed, reason);
1336
1338
  }
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;
1339
+ return;
1351
1340
  }
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
1341
+ if (msg.payload.status.sessionId !== session.id) {
1342
+ const reason = `session id mismatch: expected ${session.id}, got ${msg.payload.status.sessionId}`;
1343
+ this.rejectHandshakeResponse(session, reason, {
1344
+ ...session.loggingMetadata,
1345
+ transportMessage: msg
1366
1346
  });
1367
- this.protocolError(ProtocolError.HandshakeFailed, "session id mismatch");
1368
- return false;
1347
+ return;
1369
1348
  }
1370
- this.log?.debug(`handshake from ${parsed.from} ok`, {
1371
- ...conn.loggingMetadata,
1372
- clientId: this.clientId,
1373
- connectedTo: parsed.from,
1374
- transportMessage: parsed
1349
+ this.log?.info(`handshake from ${msg.from} ok`, {
1350
+ ...session.loggingMetadata,
1351
+ transportMessage: msg
1375
1352
  });
1376
- const { session, isTransparentReconnect } = this.getOrCreateSession({
1377
- to: parsed.from,
1378
- conn,
1379
- sessionId: parsed.payload.status.sessionId
1353
+ const connectedSession = SessionStateGraph.transition.HandshakingToConnected(session, {
1354
+ onConnectionErrored: (err) => {
1355
+ const errStr = coerceErrorString(err);
1356
+ this.log?.warn(
1357
+ `connection to ${connectedSession.to} errored: ${errStr}`,
1358
+ connectedSession.loggingMetadata
1359
+ );
1360
+ },
1361
+ onConnectionClosed: () => {
1362
+ this.log?.info(
1363
+ `connection to ${connectedSession.to} closed`,
1364
+ connectedSession.loggingMetadata
1365
+ );
1366
+ this.onConnClosed(connectedSession);
1367
+ },
1368
+ onMessage: (msg2) => this.handleMsg(msg2),
1369
+ onInvalidMessage: (reason) => {
1370
+ this.deleteSession(connectedSession);
1371
+ this.protocolError(ProtocolError.MessageOrderingViolated, reason);
1372
+ }
1380
1373
  });
1381
- this.onConnect(conn, session, isTransparentReconnect);
1382
- this.retryBudget.startRestoringBudget(session.to);
1383
- return session;
1374
+ this.updateSession(connectedSession);
1375
+ this.retryBudget.startRestoringBudget(connectedSession.to);
1384
1376
  }
1385
1377
  /**
1386
1378
  * Manually attempts to connect to a client.
1387
1379
  * @param to The client ID of the node to connect to.
1388
1380
  */
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
- });
1381
+ connect(to) {
1382
+ let session = this.sessions.get(to);
1383
+ session ??= this.createUnconnectedSession(to);
1384
+ if (session.state !== "NoConnection" /* NoConnection */) {
1385
+ this.log?.debug(
1386
+ `session to ${to} has state ${session.state}, skipping connect attempt`,
1387
+ session.loggingMetadata
1388
+ );
1395
1389
  return;
1396
1390
  }
1397
- const canProceedWithConnection = () => this.getStatus() === "open";
1398
- if (!canProceedWithConnection()) {
1391
+ if (this.getStatus() !== "open") {
1399
1392
  this.log?.info(
1400
1393
  `transport state is no longer open, cancelling attempt to connect to ${to}`,
1401
- { clientId: this.clientId, connectedTo: to }
1394
+ session.loggingMetadata
1402
1395
  );
1403
1396
  return;
1404
1397
  }
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
- }
1472
- );
1398
+ if (!this.retryBudget.hasBudget(to)) {
1399
+ const budgetConsumed = this.retryBudget.getBudgetConsumed(to);
1400
+ const errMsg = `tried to connect to ${to} but retry budget exceeded (more than ${budgetConsumed} attempts in the last ${this.retryBudget.totalBudgetRestoreTime}ms)`;
1401
+ this.log?.error(errMsg, session.loggingMetadata);
1402
+ this.protocolError(ProtocolError.RetriesExceeded, errMsg);
1403
+ return;
1473
1404
  }
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
- }
1405
+ let sleep = Promise.resolve();
1406
+ const backoffMs = this.retryBudget.getBackoffMs(to);
1407
+ if (backoffMs > 0) {
1408
+ sleep = new Promise((resolve) => setTimeout(resolve, backoffMs));
1491
1409
  }
1492
- }
1493
- deleteSession({
1494
- session,
1495
- closeHandshakingConnection,
1496
- handshakingConn
1497
- }) {
1498
- this.inflightConnectionPromises.delete(session.to);
1499
- super.deleteSession({
1500
- session,
1501
- closeHandshakingConnection,
1502
- handshakingConn
1410
+ this.log?.info(
1411
+ `attempting connection to ${to} (${backoffMs}ms backoff)`,
1412
+ session.loggingMetadata
1413
+ );
1414
+ this.retryBudget.consumeBudget(to);
1415
+ const reconnectPromise = tracing_default.startActiveSpan("connect", async (span) => {
1416
+ try {
1417
+ span.addEvent("backoff", { backoffMs });
1418
+ await sleep;
1419
+ if (this.getStatus() !== "open") {
1420
+ throw new Error("transport state is no longer open");
1421
+ }
1422
+ span.addEvent("connecting");
1423
+ return await this.createNewOutgoingConnection(to);
1424
+ } catch (err) {
1425
+ const errStr = coerceErrorString(err);
1426
+ span.recordException(errStr);
1427
+ span.setStatus({ code: import_api3.SpanStatusCode.ERROR });
1428
+ throw err;
1429
+ } finally {
1430
+ span.end();
1431
+ }
1503
1432
  });
1433
+ const connectingSession = SessionStateGraph.transition.NoConnectionToConnecting(
1434
+ session,
1435
+ reconnectPromise,
1436
+ {
1437
+ onConnectionEstablished: (conn) => {
1438
+ this.log?.debug(
1439
+ `connection to ${connectingSession.to} established`,
1440
+ connectingSession.loggingMetadata
1441
+ );
1442
+ this.onConnectionEstablished(connectingSession, conn);
1443
+ },
1444
+ onConnectionFailed: (error) => {
1445
+ const errStr = coerceErrorString(error);
1446
+ this.log?.error(
1447
+ `error connecting to ${connectingSession.to}: ${errStr}`,
1448
+ connectingSession.loggingMetadata
1449
+ );
1450
+ this.onConnectingFailed(connectingSession);
1451
+ },
1452
+ onConnectionTimeout: () => {
1453
+ this.log?.error(
1454
+ `connection to ${connectingSession.to} timed out`,
1455
+ connectingSession.loggingMetadata
1456
+ );
1457
+ this.onConnectingFailed(connectingSession);
1458
+ }
1459
+ }
1460
+ );
1461
+ this.updateSession(connectingSession);
1504
1462
  }
1505
- async sendHandshake(to, conn) {
1463
+ async sendHandshake(session) {
1506
1464
  let metadata = void 0;
1507
1465
  if (this.handshakeExtensions) {
1508
1466
  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
1467
  }
1530
- const { session } = this.getOrCreateSession({ to, handshakingConn: conn });
1531
1468
  const requestMsg = handshakeRequestMessage({
1532
1469
  from: this.clientId,
1533
- to,
1470
+ to: session.to,
1534
1471
  sessionId: session.id,
1535
1472
  expectedSessionState: {
1536
- reconnect: session.advertisedSessionId !== void 0,
1537
- nextExpectedSeq: session.nextExpectedSeq
1473
+ nextExpectedSeq: session.ack,
1474
+ nextSentSeq: session.nextSeq()
1538
1475
  },
1539
1476
  metadata,
1540
1477
  tracing: getPropagationContext(session.telemetry.ctx)
1541
1478
  });
1542
- this.log?.debug(`sending handshake request to ${to}`, {
1543
- ...conn.loggingMetadata,
1544
- clientId: this.clientId,
1545
- connectedTo: to,
1479
+ this.log?.debug(`sending handshake request to ${session.to}`, {
1480
+ ...session.loggingMetadata,
1546
1481
  transportMessage: requestMsg
1547
1482
  });
1548
- conn.send(this.codec.toBuffer(requestMsg));
1549
- return true;
1483
+ session.sendHandshake(requestMsg);
1550
1484
  }
1551
1485
  close() {
1552
1486
  this.retryBudget.close();
@@ -1554,10 +1488,79 @@ var ClientTransport = class extends Transport {
1554
1488
  }
1555
1489
  };
1556
1490
 
1491
+ // transport/connection.ts
1492
+ var Connection = class {
1493
+ id;
1494
+ telemetry;
1495
+ constructor() {
1496
+ this.id = `conn-${generateId()}`;
1497
+ }
1498
+ get loggingMetadata() {
1499
+ const metadata = { connId: this.id };
1500
+ const spanContext = this.telemetry?.span.spanContext();
1501
+ if (this.telemetry?.span.isRecording() && spanContext) {
1502
+ metadata.telemetry = {
1503
+ traceId: spanContext.traceId,
1504
+ spanId: spanContext.spanId
1505
+ };
1506
+ }
1507
+ return metadata;
1508
+ }
1509
+ // can't use event emitter because we need this to work in both node + browser
1510
+ _dataListeners = /* @__PURE__ */ new Set();
1511
+ _closeListeners = /* @__PURE__ */ new Set();
1512
+ _errorListeners = /* @__PURE__ */ new Set();
1513
+ get dataListeners() {
1514
+ return [...this._dataListeners];
1515
+ }
1516
+ get closeListeners() {
1517
+ return [...this._closeListeners];
1518
+ }
1519
+ get errorListeners() {
1520
+ return [...this._errorListeners];
1521
+ }
1522
+ /**
1523
+ * Handle adding a callback for when a message is received.
1524
+ * @param msg The message that was received.
1525
+ */
1526
+ addDataListener(cb) {
1527
+ this._dataListeners.add(cb);
1528
+ }
1529
+ removeDataListener(cb) {
1530
+ this._dataListeners.delete(cb);
1531
+ }
1532
+ /**
1533
+ * Handle adding a callback for when the connection is closed.
1534
+ * This should also be called if an error happens and after notifying all the error listeners.
1535
+ * @param cb The callback to call when the connection is closed.
1536
+ */
1537
+ addCloseListener(cb) {
1538
+ this._closeListeners.add(cb);
1539
+ }
1540
+ removeCloseListener(cb) {
1541
+ this._closeListeners.delete(cb);
1542
+ }
1543
+ /**
1544
+ * Handle adding a callback for when an error is received.
1545
+ * This should only be used for this.logging errors, all cleanup
1546
+ * should be delegated to addCloseListener.
1547
+ *
1548
+ * The implementer should take care such that the implemented
1549
+ * connection will call both the close and error callbacks
1550
+ * on an error.
1551
+ *
1552
+ * @param cb The callback to call when an error is received.
1553
+ */
1554
+ addErrorListener(cb) {
1555
+ this._errorListeners.add(cb);
1556
+ }
1557
+ removeErrorListener(cb) {
1558
+ this._errorListeners.delete(cb);
1559
+ }
1560
+ };
1561
+
1557
1562
  // transport/impls/ws/connection.ts
1558
1563
  var WebSocketConnection = class extends Connection {
1559
- errorCb = null;
1560
- closeCb = null;
1561
1564
  ws;
1562
1565
  constructor(ws) {
1563
1566
  super();
@@ -1568,37 +1571,30 @@ var WebSocketConnection = class extends Connection {
1568
1571
  didError = true;
1569
1572
  };
1570
1573
  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
- )
1574
+ if (didError) {
1575
+ const err = new Error(
1576
+ `websocket closed with code and reason: ${code} - ${reason}`
1576
1577
  );
1578
+ for (const cb of this.errorListeners) {
1579
+ cb(err);
1580
+ }
1577
1581
  }
1578
- if (this.closeCb) {
1579
- this.closeCb();
1582
+ for (const cb of this.closeListeners) {
1583
+ cb();
1584
+ }
1585
+ };
1586
+ this.ws.onmessage = (msg) => {
1587
+ for (const cb of this.dataListeners) {
1588
+ cb(msg.data);
1580
1589
  }
1581
1590
  };
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
1591
  }
1595
1592
  send(payload) {
1596
- if (this.ws.readyState === this.ws.OPEN) {
1597
- this.ws.send(payload);
1598
- return true;
1599
- } else {
1593
+ if (this.ws.readyState !== this.ws.OPEN) {
1600
1594
  return false;
1601
1595
  }
1596
+ this.ws.send(payload);
1597
+ return true;
1602
1598
  }
1603
1599
  close() {
1604
1600
  this.ws.close();
@@ -1652,7 +1648,6 @@ var WebSocketClientTransport = class extends ClientTransport {
1652
1648
  clientId: this.clientId,
1653
1649
  connectedTo: to
1654
1650
  });
1655
- this.handleConnection(conn, to);
1656
1651
  return conn;
1657
1652
  }
1658
1653
  };