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