@langchain/langgraph-sdk 1.9.20 → 1.9.22

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 (132) hide show
  1. package/dist/client/base.cjs +4 -1
  2. package/dist/client/base.cjs.map +1 -1
  3. package/dist/client/base.d.cts +2 -0
  4. package/dist/client/base.d.cts.map +1 -1
  5. package/dist/client/base.d.ts +2 -0
  6. package/dist/client/base.d.ts.map +1 -1
  7. package/dist/client/base.js +5 -2
  8. package/dist/client/base.js.map +1 -1
  9. package/dist/client/index.cjs +1 -1
  10. package/dist/client/index.js +1 -1
  11. package/dist/client/runs/index.cjs +2 -0
  12. package/dist/client/runs/index.cjs.map +1 -1
  13. package/dist/client/runs/index.d.cts +15 -0
  14. package/dist/client/runs/index.d.cts.map +1 -1
  15. package/dist/client/runs/index.d.ts +15 -0
  16. package/dist/client/runs/index.d.ts.map +1 -1
  17. package/dist/client/runs/index.js +2 -0
  18. package/dist/client/runs/index.js.map +1 -1
  19. package/dist/client/stream/error.cjs +21 -0
  20. package/dist/client/stream/error.cjs.map +1 -1
  21. package/dist/client/stream/error.js +21 -1
  22. package/dist/client/stream/error.js.map +1 -1
  23. package/dist/client/stream/index.cjs +24 -1
  24. package/dist/client/stream/index.cjs.map +1 -1
  25. package/dist/client/stream/index.d.cts.map +1 -1
  26. package/dist/client/stream/index.d.ts.map +1 -1
  27. package/dist/client/stream/index.js +24 -1
  28. package/dist/client/stream/index.js.map +1 -1
  29. package/dist/client/stream/transport/agent-server.cjs +8 -2
  30. package/dist/client/stream/transport/agent-server.cjs.map +1 -1
  31. package/dist/client/stream/transport/agent-server.d.cts +17 -2
  32. package/dist/client/stream/transport/agent-server.d.cts.map +1 -1
  33. package/dist/client/stream/transport/agent-server.d.ts +17 -2
  34. package/dist/client/stream/transport/agent-server.d.ts.map +1 -1
  35. package/dist/client/stream/transport/agent-server.js +8 -2
  36. package/dist/client/stream/transport/agent-server.js.map +1 -1
  37. package/dist/client/stream/transport/http.cjs +81 -21
  38. package/dist/client/stream/transport/http.cjs.map +1 -1
  39. package/dist/client/stream/transport/http.d.cts +22 -7
  40. package/dist/client/stream/transport/http.d.cts.map +1 -1
  41. package/dist/client/stream/transport/http.d.ts +22 -7
  42. package/dist/client/stream/transport/http.d.ts.map +1 -1
  43. package/dist/client/stream/transport/http.js +83 -23
  44. package/dist/client/stream/transport/http.js.map +1 -1
  45. package/dist/client/stream/transport/index.cjs +2 -1
  46. package/dist/client/stream/transport/index.js +2 -1
  47. package/dist/client/stream/transport/types.d.cts +85 -5
  48. package/dist/client/stream/transport/types.d.cts.map +1 -1
  49. package/dist/client/stream/transport/types.d.ts +85 -5
  50. package/dist/client/stream/transport/types.d.ts.map +1 -1
  51. package/dist/client/stream/transport/utils.cjs +19 -0
  52. package/dist/client/stream/transport/utils.cjs.map +1 -1
  53. package/dist/client/stream/transport/utils.js +19 -1
  54. package/dist/client/stream/transport/utils.js.map +1 -1
  55. package/dist/client/stream/transport/websocket.cjs +125 -21
  56. package/dist/client/stream/transport/websocket.cjs.map +1 -1
  57. package/dist/client/stream/transport/websocket.d.cts +32 -4
  58. package/dist/client/stream/transport/websocket.d.cts.map +1 -1
  59. package/dist/client/stream/transport/websocket.d.ts +32 -4
  60. package/dist/client/stream/transport/websocket.d.ts.map +1 -1
  61. package/dist/client/stream/transport/websocket.js +126 -23
  62. package/dist/client/stream/transport/websocket.js.map +1 -1
  63. package/dist/client/stream/transport.d.cts +20 -3
  64. package/dist/client/stream/transport.d.cts.map +1 -1
  65. package/dist/client/stream/transport.d.ts +20 -3
  66. package/dist/client/stream/transport.d.ts.map +1 -1
  67. package/dist/client/stream/types.d.cts +31 -0
  68. package/dist/client/stream/types.d.cts.map +1 -1
  69. package/dist/client/stream/types.d.ts +31 -0
  70. package/dist/client/stream/types.d.ts.map +1 -1
  71. package/dist/client/threads/index.cjs +36 -17
  72. package/dist/client/threads/index.cjs.map +1 -1
  73. package/dist/client/threads/index.js +35 -16
  74. package/dist/client/threads/index.js.map +1 -1
  75. package/dist/client.cjs +1 -1
  76. package/dist/client.js +1 -1
  77. package/dist/index.cjs +1 -1
  78. package/dist/index.js +1 -1
  79. package/dist/react-ui/server/server.cjs +2 -2
  80. package/dist/react-ui/server/server.cjs.map +1 -1
  81. package/dist/react-ui/server/server.js +1 -1
  82. package/dist/react-ui/server/server.js.map +1 -1
  83. package/dist/stream/controller.cjs +21 -3
  84. package/dist/stream/controller.cjs.map +1 -1
  85. package/dist/stream/controller.d.cts.map +1 -1
  86. package/dist/stream/controller.d.ts.map +1 -1
  87. package/dist/stream/controller.js +20 -2
  88. package/dist/stream/controller.js.map +1 -1
  89. package/dist/stream/index.cjs +2 -0
  90. package/dist/stream/index.d.cts +2 -1
  91. package/dist/stream/index.d.ts +2 -1
  92. package/dist/stream/index.js +2 -1
  93. package/dist/stream/projections/channel-effect.cjs +52 -0
  94. package/dist/stream/projections/channel-effect.cjs.map +1 -0
  95. package/dist/stream/projections/channel-effect.d.cts +35 -0
  96. package/dist/stream/projections/channel-effect.d.cts.map +1 -0
  97. package/dist/stream/projections/channel-effect.d.ts +35 -0
  98. package/dist/stream/projections/channel-effect.d.ts.map +1 -0
  99. package/dist/stream/projections/channel-effect.js +52 -0
  100. package/dist/stream/projections/channel-effect.js.map +1 -0
  101. package/dist/stream/projections/index.cjs +1 -0
  102. package/dist/stream/projections/index.d.ts +1 -0
  103. package/dist/stream/projections/index.js +1 -0
  104. package/dist/stream/root-message-projection.cjs +55 -0
  105. package/dist/stream/root-message-projection.cjs.map +1 -1
  106. package/dist/stream/root-message-projection.js +55 -0
  107. package/dist/stream/root-message-projection.js.map +1 -1
  108. package/dist/stream/submit-coordinator.cjs +3 -3
  109. package/dist/stream/submit-coordinator.cjs.map +1 -1
  110. package/dist/stream/submit-coordinator.js +1 -1
  111. package/dist/stream/submit-coordinator.js.map +1 -1
  112. package/dist/types.d.cts +20 -0
  113. package/dist/types.d.cts.map +1 -1
  114. package/dist/types.d.ts +20 -0
  115. package/dist/types.d.ts.map +1 -1
  116. package/dist/ui/branching.d.cts +1 -1
  117. package/dist/ui/branching.d.ts +1 -1
  118. package/dist/ui/orchestrator.d.cts +1 -1
  119. package/dist/ui/orchestrator.d.cts.map +1 -1
  120. package/dist/ui/orchestrator.d.ts +1 -1
  121. package/dist/ui/orchestrator.d.ts.map +1 -1
  122. package/dist/utils/index.d.cts +1 -1
  123. package/dist/utils/index.d.ts +1 -1
  124. package/dist/utils/stream.cjs +94 -0
  125. package/dist/utils/stream.cjs.map +1 -1
  126. package/dist/utils/stream.d.cts +16 -2
  127. package/dist/utils/stream.d.cts.map +1 -1
  128. package/dist/utils/stream.d.ts +16 -2
  129. package/dist/utils/stream.d.ts.map +1 -1
  130. package/dist/utils/stream.js +94 -1
  131. package/dist/utils/stream.js.map +1 -1
  132. package/package.json +6 -5
@@ -1,10 +1,29 @@
1
+ const require_error = require("../error.cjs");
1
2
  const require_queue = require("./queue.cjs");
2
3
  const require_utils = require("./utils.cjs");
3
4
  //#region src/client/stream/transport/websocket.ts
5
+ const WEB_SOCKET_CONNECTING = 0;
6
+ const WEB_SOCKET_OPEN = 1;
7
+ const WEB_SOCKET_CLOSED = 3;
8
+ /**
9
+ * Exponential backoff with jitter for WebSocket reconnect. Mirrors
10
+ * {@link streamWithRetry} in `utils/stream.ts` (capped at 5s + 1s jitter).
11
+ */
12
+ function webSocketReconnectDelayMs(attempt) {
13
+ return Math.min(1e3 * 2 ** (attempt - 1), 5e3) + Math.random() * 1e3;
14
+ }
4
15
  /**
5
16
  * Transport adapter that speaks the thread-centric protocol over a
6
- * bidirectional WebSocket. Bound to a specific `threadId` the socket
7
- * connects to `ws://.../threads/:thread_id/stream/events`.
17
+ * bidirectional WebSocket. Bound to a `threadId` at construction or later
18
+ * via {@link setThreadId} — the socket connects to
19
+ * `ws://.../threads/:thread_id/stream/events`.
20
+ *
21
+ * On unexpected disconnect the adapter reconnects with exponential
22
+ * backoff (see {@link ProtocolWebSocketTransportOptions.maxReconnectAttempts}).
23
+ * The server replays buffered events on the new socket; the SDK
24
+ * deduplicates by `event_id`. {@link ProtocolWebSocketTransportOptions.onReconnected}
25
+ * runs after each successful reconnect so `ThreadStream` can re-issue
26
+ * `subscription.subscribe` commands.
8
27
  */
9
28
  var ProtocolWebSocketTransportAdapter = class {
10
29
  threadId;
@@ -13,30 +32,63 @@ var ProtocolWebSocketTransportAdapter = class {
13
32
  defaultHeaders;
14
33
  onRequest;
15
34
  webSocketFactory;
16
- streamUrl;
35
+ paths;
36
+ maxReconnectAttempts;
37
+ onReconnect;
38
+ reconnectDelayMs;
39
+ onReconnected;
17
40
  pending = /* @__PURE__ */ new Map();
18
41
  socket = null;
19
42
  closed = false;
20
43
  intentionalClose = false;
44
+ reconnectInFlight = null;
21
45
  constructor(options) {
22
46
  this.apiUrl = options.apiUrl;
23
- this.threadId = options.threadId;
47
+ this.threadId = options.threadId ?? "";
24
48
  this.defaultHeaders = options.defaultHeaders;
25
49
  this.onRequest = options.onRequest;
26
50
  this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
27
- this.streamUrl = options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;
51
+ this.paths = options.paths;
52
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
53
+ this.onReconnect = options.onReconnect;
54
+ this.onReconnected = options.onReconnected;
55
+ this.reconnectDelayMs = options.reconnectDelayMs ?? webSocketReconnectDelayMs;
56
+ }
57
+ /** {@inheritDoc TransportAdapter.setThreadId} */
58
+ setThreadId(threadId) {
59
+ if (threadId === this.threadId) return;
60
+ if (this.reconnectInFlight != null || this.socket != null && this.socket.readyState !== WEB_SOCKET_CLOSED) throw new Error("Protocol WebSocket transport cannot be rebound to a different thread while the socket is open. Close the current stream and create a new WebSocket transport for the new thread.");
61
+ this.threadId = threadId;
62
+ }
63
+ /**
64
+ * Socket URL derives from the currently-bound thread so a single adapter
65
+ * can follow {@link setThreadId} re-binds; the next {@link open} connects
66
+ * to the new thread. A fixed `paths.stream` string overrides the default.
67
+ */
68
+ get streamUrl() {
69
+ return require_utils.resolveProtocolPath(this.paths?.stream, this.threadId, (id) => `/threads/${id}/stream/events`);
70
+ }
71
+ /**
72
+ * Register a callback invoked after each successful reconnect. Used
73
+ * by {@link ThreadStream} to re-send active `subscription.subscribe`
74
+ * commands.
75
+ */
76
+ setOnReconnected(handler) {
77
+ this.onReconnected = handler;
28
78
  }
29
79
  async open() {
30
- if (this.socket != null) return;
80
+ if (this.closed) throw new Error("Protocol WebSocket transport is closed.");
81
+ if (this.socket?.readyState === WEB_SOCKET_OPEN) return;
82
+ if (this.socket != null) {
83
+ this.#detachSocket(this.socket);
84
+ this.socket = null;
85
+ }
31
86
  this.assertBrowserSafeTransportConfig();
32
87
  const wsUrl = require_utils.toWebSocketUrl(require_utils.toAbsoluteUrl(this.apiUrl, this.streamUrl).toString());
33
88
  const socket = this.webSocketFactory(wsUrl);
34
89
  this.socket = socket;
35
- this.closed = false;
36
90
  this.intentionalClose = false;
37
- socket.addEventListener("message", this.handleMessage);
38
- socket.addEventListener("close", this.handleClose);
39
- socket.addEventListener("error", this.handleSocketError);
91
+ this.#attachSocket(socket);
40
92
  await new Promise((resolve, reject) => {
41
93
  const onOpen = () => {
42
94
  cleanup();
@@ -80,8 +132,9 @@ var ProtocolWebSocketTransportAdapter = class {
80
132
  const socket = this.socket;
81
133
  this.socket = null;
82
134
  if (!socket) return;
135
+ this.#detachSocket(socket);
83
136
  await new Promise((resolve) => {
84
- if (socket.readyState === WebSocket.CLOSED) {
137
+ if (socket.readyState === WEB_SOCKET_CLOSED) {
85
138
  resolve();
86
139
  return;
87
140
  }
@@ -90,7 +143,7 @@ var ProtocolWebSocketTransportAdapter = class {
90
143
  resolve();
91
144
  };
92
145
  socket.addEventListener("close", onClose, { once: true });
93
- if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close();
146
+ if (socket.readyState === WEB_SOCKET_OPEN || socket.readyState === WEB_SOCKET_CONNECTING) socket.close();
94
147
  else resolve();
95
148
  });
96
149
  }
@@ -98,8 +151,12 @@ var ProtocolWebSocketTransportAdapter = class {
98
151
  if (require_utils.hasHeaders(this.defaultHeaders) || this.onRequest != null) throw new Error("Browser WebSocket protocol transport does not support defaultHeaders or onRequest hooks. Supply a custom protocolWebSocketFactory if you need custom WebSocket setup.");
99
152
  }
100
153
  async sendCommand(command) {
101
- const socket = this.socket;
102
- if (socket == null || socket.readyState !== WebSocket.OPEN) throw new Error("Protocol WebSocket is not open.");
154
+ let socket = this.socket;
155
+ if (this.reconnectInFlight != null && (socket == null || socket.readyState !== WEB_SOCKET_OPEN)) {
156
+ await this.reconnectInFlight.catch(() => void 0);
157
+ socket = this.socket;
158
+ }
159
+ if (socket == null || socket.readyState !== WEB_SOCKET_OPEN) throw new Error("Protocol WebSocket is not open.");
103
160
  return await new Promise((resolve, reject) => {
104
161
  this.pending.set(command.id, {
105
162
  resolve,
@@ -113,6 +170,16 @@ var ProtocolWebSocketTransportAdapter = class {
113
170
  }
114
171
  });
115
172
  }
173
+ #attachSocket(socket) {
174
+ socket.addEventListener("message", this.handleMessage);
175
+ socket.addEventListener("close", this.handleClose);
176
+ socket.addEventListener("error", this.handleSocketError);
177
+ }
178
+ #detachSocket(socket) {
179
+ socket.removeEventListener("message", this.handleMessage);
180
+ socket.removeEventListener("close", this.handleClose);
181
+ socket.removeEventListener("error", this.handleSocketError);
182
+ }
116
183
  handleMessage = (event) => {
117
184
  let payload;
118
185
  try {
@@ -131,25 +198,62 @@ var ProtocolWebSocketTransportAdapter = class {
131
198
  if (require_utils.isRecord(payload) && payload.type === "event") this.queue.push(payload);
132
199
  };
133
200
  handleClose = () => {
201
+ const socket = this.socket;
202
+ if (socket != null) this.#detachSocket(socket);
134
203
  this.socket = null;
135
204
  if (this.intentionalClose || this.closed) {
136
205
  this.queue.close();
137
206
  return;
138
207
  }
139
- const error = /* @__PURE__ */ new Error("Protocol WebSocket closed unexpectedly.");
140
- for (const { reject } of this.pending.values()) reject(error);
141
- this.pending.clear();
142
- this.queue.close(error);
208
+ this.#handleUnexpectedDisconnect(/* @__PURE__ */ new Error("Protocol WebSocket closed unexpectedly."));
143
209
  };
144
210
  handleSocketError = () => {
145
211
  if (this.closed || this.intentionalClose) return;
146
- const error = /* @__PURE__ */ new Error("Protocol WebSocket encountered an error.");
212
+ this.#handleUnexpectedDisconnect(/* @__PURE__ */ new Error("Protocol WebSocket encountered an error."));
213
+ };
214
+ #handleUnexpectedDisconnect(cause) {
215
+ const error = require_utils.toError(cause);
147
216
  for (const { reject } of this.pending.values()) reject(error);
148
217
  this.pending.clear();
149
- this.queue.close(error);
150
- };
218
+ if (this.maxReconnectAttempts <= 0) {
219
+ this.queue.close(error);
220
+ return;
221
+ }
222
+ this.#scheduleReconnect(cause);
223
+ }
224
+ #scheduleReconnect(cause) {
225
+ if (this.closed || this.intentionalClose) return;
226
+ if (this.reconnectInFlight != null) return;
227
+ this.reconnectInFlight = this.#runReconnectLoop(cause).finally(() => {
228
+ this.reconnectInFlight = null;
229
+ });
230
+ }
231
+ async #runReconnectLoop(initialCause) {
232
+ let lastError = initialCause;
233
+ for (let attempt = 1; attempt <= this.maxReconnectAttempts; attempt += 1) {
234
+ if (this.closed || this.intentionalClose) return;
235
+ this.onReconnect?.({
236
+ attempt,
237
+ cause: lastError
238
+ });
239
+ const delay = this.reconnectDelayMs(attempt);
240
+ if (delay > 0) await new Promise((resolve) => {
241
+ setTimeout(resolve, delay);
242
+ });
243
+ if (this.closed || this.intentionalClose) return;
244
+ try {
245
+ await this.open();
246
+ if (this.onReconnected) await this.onReconnected();
247
+ return;
248
+ } catch (error) {
249
+ lastError = error;
250
+ }
251
+ }
252
+ this.queue.close(new require_error.MaxWebSocketReconnectAttemptsError(this.maxReconnectAttempts, lastError));
253
+ }
151
254
  };
152
255
  //#endregion
153
256
  exports.ProtocolWebSocketTransportAdapter = ProtocolWebSocketTransportAdapter;
257
+ exports.webSocketReconnectDelayMs = webSocketReconnectDelayMs;
154
258
 
155
259
  //# sourceMappingURL=websocket.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.cjs","names":["AsyncQueue","toWebSocketUrl","toAbsoluteUrl","hasHeaders","toError","isRecord"],"sources":["../../../../src/client/stream/transport/websocket.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport {\n toAbsoluteUrl,\n toWebSocketUrl,\n isRecord,\n hasHeaders,\n toError,\n} from \"./utils.js\";\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n PendingResponse,\n ProtocolWebSocketTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter } from \"../transport.js\";\n\n/**\n * Transport adapter that speaks the thread-centric protocol over a\n * bidirectional WebSocket. Bound to a specific `threadId` — the socket\n * connects to `ws://.../threads/:thread_id/stream/events`.\n */\nexport class ProtocolWebSocketTransportAdapter implements TransportAdapter {\n readonly threadId: string;\n\n private readonly queue = new AsyncQueue<Message>();\n\n private readonly apiUrl: string;\n\n private readonly defaultHeaders?: Record<string, HeaderValue>;\n\n private readonly onRequest?: ProtocolRequestHook;\n\n private readonly webSocketFactory: (url: string) => WebSocket;\n\n private readonly streamUrl: string;\n\n private readonly pending = new Map<number, PendingResponse>();\n\n private socket: WebSocket | null = null;\n\n private closed = false;\n\n private intentionalClose = false;\n\n constructor(options: ProtocolWebSocketTransportOptions) {\n this.apiUrl = options.apiUrl;\n this.threadId = options.threadId;\n this.defaultHeaders = options.defaultHeaders;\n this.onRequest = options.onRequest;\n this.webSocketFactory =\n options.webSocketFactory ?? ((url) => new WebSocket(url));\n this.streamUrl =\n options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;\n }\n\n async open(): Promise<void> {\n if (this.socket != null) return;\n this.assertBrowserSafeTransportConfig();\n\n const wsUrl = toWebSocketUrl(\n toAbsoluteUrl(this.apiUrl, this.streamUrl).toString()\n );\n const socket = this.webSocketFactory(wsUrl);\n this.socket = socket;\n this.closed = false;\n this.intentionalClose = false;\n\n socket.addEventListener(\"message\", this.handleMessage);\n socket.addEventListener(\"close\", this.handleClose);\n socket.addEventListener(\"error\", this.handleSocketError);\n\n await new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n cleanup();\n resolve();\n };\n const onError = () => {\n cleanup();\n reject(new Error(\"Failed to open protocol WebSocket.\"));\n };\n const cleanup = () => {\n socket.removeEventListener(\"open\", onOpen);\n socket.removeEventListener(\"error\", onError);\n };\n socket.addEventListener(\"open\", onOpen, { once: true });\n socket.addEventListener(\"error\", onError, { once: true });\n });\n }\n\n async send(\n command: Command\n ): Promise<CommandResponse | ErrorResponse | void> {\n return await this.sendCommand(command);\n }\n\n events(): AsyncIterable<Message> {\n const queue = this.queue;\n return {\n [Symbol.asyncIterator]: () => ({\n next: async () => await queue.shift(),\n return: async () => {\n queue.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n this.intentionalClose = true;\n\n for (const { reject } of this.pending.values()) {\n reject(new Error(\"Protocol WebSocket connection closed.\"));\n }\n this.pending.clear();\n this.queue.close();\n\n const socket = this.socket;\n this.socket = null;\n if (!socket) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n if (socket.readyState === WebSocket.CLOSED) {\n resolve();\n return;\n }\n\n const onClose = () => {\n socket.removeEventListener(\"close\", onClose);\n resolve();\n };\n\n socket.addEventListener(\"close\", onClose, { once: true });\n if (\n socket.readyState === WebSocket.OPEN ||\n socket.readyState === WebSocket.CONNECTING\n ) {\n socket.close();\n } else {\n resolve();\n }\n });\n }\n\n private assertBrowserSafeTransportConfig(): void {\n if (hasHeaders(this.defaultHeaders) || this.onRequest != null) {\n throw new Error(\n \"Browser WebSocket protocol transport does not support defaultHeaders or onRequest hooks. Supply a custom protocolWebSocketFactory if you need custom WebSocket setup.\"\n );\n }\n }\n\n private async sendCommand(\n command: Command\n ): Promise<CommandResponse | ErrorResponse> {\n const socket = this.socket;\n if (socket == null || socket.readyState !== WebSocket.OPEN) {\n throw new Error(\"Protocol WebSocket is not open.\");\n }\n\n return await new Promise<CommandResponse | ErrorResponse>(\n (resolve, reject) => {\n this.pending.set(command.id, { resolve, reject });\n\n try {\n socket.send(JSON.stringify(command));\n } catch (error) {\n this.pending.delete(command.id);\n reject(toError(error));\n }\n }\n );\n }\n\n private readonly handleMessage = (event: MessageEvent): void => {\n let payload: unknown;\n try {\n payload = JSON.parse(String(event.data));\n } catch {\n return;\n }\n\n if (\n isRecord(payload) &&\n typeof payload.id === \"number\" &&\n (payload.type === \"success\" || payload.type === \"error\")\n ) {\n const pending = this.pending.get(payload.id);\n if (pending) {\n this.pending.delete(payload.id);\n pending.resolve(payload as CommandResponse | ErrorResponse);\n }\n return;\n }\n\n if (isRecord(payload) && payload.type === \"event\") {\n this.queue.push(payload as Message);\n }\n };\n\n private readonly handleClose = (): void => {\n this.socket = null;\n\n if (this.intentionalClose || this.closed) {\n this.queue.close();\n return;\n }\n\n const error = new Error(\"Protocol WebSocket closed unexpectedly.\");\n for (const { reject } of this.pending.values()) {\n reject(error);\n }\n this.pending.clear();\n this.queue.close(error);\n };\n\n private readonly handleSocketError = (): void => {\n if (this.closed || this.intentionalClose) {\n return;\n }\n\n const error = new Error(\"Protocol WebSocket encountered an error.\");\n for (const { reject } of this.pending.values()) {\n reject(error);\n }\n this.pending.clear();\n this.queue.close(error);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"websocket.cjs","names":["AsyncQueue","resolveProtocolPath","#detachSocket","toWebSocketUrl","toAbsoluteUrl","#attachSocket","hasHeaders","toError","isRecord","#handleUnexpectedDisconnect","#scheduleReconnect","#runReconnectLoop","MaxWebSocketReconnectAttemptsError"],"sources":["../../../../src/client/stream/transport/websocket.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport {\n toAbsoluteUrl,\n toWebSocketUrl,\n isRecord,\n hasHeaders,\n toError,\n resolveProtocolPath,\n} from \"./utils.js\";\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n PendingResponse,\n ProtocolTransportPaths,\n ProtocolWebSocketTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter } from \"../transport.js\";\nimport { MaxWebSocketReconnectAttemptsError } from \"../error.js\";\n\nconst WEB_SOCKET_CONNECTING = 0;\nconst WEB_SOCKET_OPEN = 1;\nconst WEB_SOCKET_CLOSED = 3;\n\n/**\n * Reconnect tuning for {@link ProtocolWebSocketTransportAdapter}. A subset\n * of {@link ProtocolWebSocketTransportOptions}.\n */\nexport interface WebSocketReconnectOptions {\n /**\n * Maximum reconnection attempts after an unexpected disconnect.\n * Defaults to 5.\n */\n maxReconnectAttempts?: number;\n\n /**\n * Invoked before each reconnect attempt (after backoff).\n */\n onReconnect?: (options: { attempt: number; cause: unknown }) => void;\n}\n\n/**\n * Exponential backoff with jitter for WebSocket reconnect. Mirrors\n * {@link streamWithRetry} in `utils/stream.ts` (capped at 5s + 1s jitter).\n */\nexport function webSocketReconnectDelayMs(attempt: number): number {\n const baseDelay = Math.min(1000 * 2 ** (attempt - 1), 5000);\n const jitter = Math.random() * 1000;\n return baseDelay + jitter;\n}\n\n/**\n * Transport adapter that speaks the thread-centric protocol over a\n * bidirectional WebSocket. Bound to a `threadId` at construction or later\n * via {@link setThreadId} — the socket connects to\n * `ws://.../threads/:thread_id/stream/events`.\n *\n * On unexpected disconnect the adapter reconnects with exponential\n * backoff (see {@link ProtocolWebSocketTransportOptions.maxReconnectAttempts}).\n * The server replays buffered events on the new socket; the SDK\n * deduplicates by `event_id`. {@link ProtocolWebSocketTransportOptions.onReconnected}\n * runs after each successful reconnect so `ThreadStream` can re-issue\n * `subscription.subscribe` commands.\n */\nexport class ProtocolWebSocketTransportAdapter implements TransportAdapter {\n threadId: string;\n\n private readonly queue = new AsyncQueue<Message>();\n\n private readonly apiUrl: string;\n\n private readonly defaultHeaders?: Record<string, HeaderValue>;\n\n private readonly onRequest?: ProtocolRequestHook;\n\n private readonly webSocketFactory: (url: string) => WebSocket;\n\n private readonly paths?: Pick<ProtocolTransportPaths, \"stream\">;\n\n private readonly maxReconnectAttempts: number;\n\n private readonly onReconnect?: ProtocolWebSocketTransportOptions[\"onReconnect\"];\n\n private readonly reconnectDelayMs: (attempt: number) => number;\n\n private onReconnected?: () => void | Promise<void>;\n\n private readonly pending = new Map<number, PendingResponse>();\n\n private socket: WebSocket | null = null;\n\n private closed = false;\n\n private intentionalClose = false;\n\n private reconnectInFlight: Promise<void> | null = null;\n\n constructor(options: ProtocolWebSocketTransportOptions) {\n this.apiUrl = options.apiUrl;\n this.threadId = options.threadId ?? \"\";\n this.defaultHeaders = options.defaultHeaders;\n this.onRequest = options.onRequest;\n this.webSocketFactory =\n options.webSocketFactory ?? ((url) => new WebSocket(url));\n this.paths = options.paths;\n this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;\n this.onReconnect = options.onReconnect;\n this.onReconnected = options.onReconnected;\n this.reconnectDelayMs =\n options.reconnectDelayMs ?? webSocketReconnectDelayMs;\n }\n\n /** {@inheritDoc TransportAdapter.setThreadId} */\n setThreadId(threadId: string): void {\n if (threadId === this.threadId) {\n return;\n }\n if (\n this.reconnectInFlight != null ||\n (this.socket != null && this.socket.readyState !== WEB_SOCKET_CLOSED)\n ) {\n throw new Error(\n \"Protocol WebSocket transport cannot be rebound to a different thread while the socket is open. Close the current stream and create a new WebSocket transport for the new thread.\"\n );\n }\n this.threadId = threadId;\n }\n\n /**\n * Socket URL derives from the currently-bound thread so a single adapter\n * can follow {@link setThreadId} re-binds; the next {@link open} connects\n * to the new thread. A fixed `paths.stream` string overrides the default.\n */\n private get streamUrl(): string {\n return resolveProtocolPath(\n this.paths?.stream,\n this.threadId,\n (id) => `/threads/${id}/stream/events`\n );\n }\n\n /**\n * Register a callback invoked after each successful reconnect. Used\n * by {@link ThreadStream} to re-send active `subscription.subscribe`\n * commands.\n */\n setOnReconnected(handler: () => void | Promise<void>): void {\n this.onReconnected = handler;\n }\n\n async open(): Promise<void> {\n if (this.closed) {\n throw new Error(\"Protocol WebSocket transport is closed.\");\n }\n if (this.socket?.readyState === WEB_SOCKET_OPEN) {\n return;\n }\n if (this.socket != null) {\n this.#detachSocket(this.socket);\n this.socket = null;\n }\n\n this.assertBrowserSafeTransportConfig();\n\n const wsUrl = toWebSocketUrl(\n toAbsoluteUrl(this.apiUrl, this.streamUrl).toString()\n );\n const socket = this.webSocketFactory(wsUrl);\n this.socket = socket;\n this.intentionalClose = false;\n\n this.#attachSocket(socket);\n\n await new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n cleanup();\n resolve();\n };\n const onError = () => {\n cleanup();\n reject(new Error(\"Failed to open protocol WebSocket.\"));\n };\n const cleanup = () => {\n socket.removeEventListener(\"open\", onOpen);\n socket.removeEventListener(\"error\", onError);\n };\n socket.addEventListener(\"open\", onOpen, { once: true });\n socket.addEventListener(\"error\", onError, { once: true });\n });\n }\n\n async send(\n command: Command\n ): Promise<CommandResponse | ErrorResponse | void> {\n return await this.sendCommand(command);\n }\n\n events(): AsyncIterable<Message> {\n const queue = this.queue;\n return {\n [Symbol.asyncIterator]: () => ({\n next: async () => await queue.shift(),\n return: async () => {\n queue.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n this.intentionalClose = true;\n\n for (const { reject } of this.pending.values()) {\n reject(new Error(\"Protocol WebSocket connection closed.\"));\n }\n this.pending.clear();\n this.queue.close();\n\n const socket = this.socket;\n this.socket = null;\n if (!socket) {\n return;\n }\n\n this.#detachSocket(socket);\n\n await new Promise<void>((resolve) => {\n if (socket.readyState === WEB_SOCKET_CLOSED) {\n resolve();\n return;\n }\n\n const onClose = () => {\n socket.removeEventListener(\"close\", onClose);\n resolve();\n };\n\n socket.addEventListener(\"close\", onClose, { once: true });\n if (\n socket.readyState === WEB_SOCKET_OPEN ||\n socket.readyState === WEB_SOCKET_CONNECTING\n ) {\n socket.close();\n } else {\n resolve();\n }\n });\n }\n\n private assertBrowserSafeTransportConfig(): void {\n if (hasHeaders(this.defaultHeaders) || this.onRequest != null) {\n throw new Error(\n \"Browser WebSocket protocol transport does not support defaultHeaders or onRequest hooks. Supply a custom protocolWebSocketFactory if you need custom WebSocket setup.\"\n );\n }\n }\n\n private async sendCommand(\n command: Command\n ): Promise<CommandResponse | ErrorResponse> {\n // Wait for an in-flight reconnect only when the socket is not usable.\n // After `open()` succeeds, `#runReconnectLoop` may still be awaiting\n // `onReconnected` (e.g. ThreadStream resubscribe). Those callbacks call\n // `sendCommand` and must not await the same `reconnectInFlight` promise.\n let socket = this.socket;\n if (\n this.reconnectInFlight != null &&\n (socket == null || socket.readyState !== WEB_SOCKET_OPEN)\n ) {\n await this.reconnectInFlight.catch(() => undefined);\n socket = this.socket;\n }\n\n if (socket == null || socket.readyState !== WEB_SOCKET_OPEN) {\n throw new Error(\"Protocol WebSocket is not open.\");\n }\n\n return await new Promise<CommandResponse | ErrorResponse>(\n (resolve, reject) => {\n this.pending.set(command.id, { resolve, reject });\n\n try {\n socket.send(JSON.stringify(command));\n } catch (error) {\n this.pending.delete(command.id);\n reject(toError(error));\n }\n }\n );\n }\n\n #attachSocket(socket: WebSocket): void {\n socket.addEventListener(\"message\", this.handleMessage);\n socket.addEventListener(\"close\", this.handleClose);\n socket.addEventListener(\"error\", this.handleSocketError);\n }\n\n #detachSocket(socket: WebSocket): void {\n socket.removeEventListener(\"message\", this.handleMessage);\n socket.removeEventListener(\"close\", this.handleClose);\n socket.removeEventListener(\"error\", this.handleSocketError);\n }\n\n private readonly handleMessage = (event: MessageEvent): void => {\n let payload: unknown;\n try {\n payload = JSON.parse(String(event.data));\n } catch {\n return;\n }\n\n if (\n isRecord(payload) &&\n typeof payload.id === \"number\" &&\n (payload.type === \"success\" || payload.type === \"error\")\n ) {\n const pending = this.pending.get(payload.id);\n if (pending) {\n this.pending.delete(payload.id);\n pending.resolve(payload as CommandResponse | ErrorResponse);\n }\n return;\n }\n\n if (isRecord(payload) && payload.type === \"event\") {\n this.queue.push(payload as Message);\n }\n };\n\n private readonly handleClose = (): void => {\n const socket = this.socket;\n if (socket != null) {\n this.#detachSocket(socket);\n }\n this.socket = null;\n\n if (this.intentionalClose || this.closed) {\n this.queue.close();\n return;\n }\n\n this.#handleUnexpectedDisconnect(\n new Error(\"Protocol WebSocket closed unexpectedly.\")\n );\n };\n\n private readonly handleSocketError = (): void => {\n if (this.closed || this.intentionalClose) {\n return;\n }\n\n this.#handleUnexpectedDisconnect(\n new Error(\"Protocol WebSocket encountered an error.\")\n );\n };\n\n #handleUnexpectedDisconnect(cause: unknown): void {\n const error = toError(cause);\n for (const { reject } of this.pending.values()) {\n reject(error);\n }\n this.pending.clear();\n\n if (this.maxReconnectAttempts <= 0) {\n this.queue.close(error);\n return;\n }\n\n this.#scheduleReconnect(cause);\n }\n\n #scheduleReconnect(cause: unknown): void {\n if (this.closed || this.intentionalClose) {\n return;\n }\n if (this.reconnectInFlight != null) {\n return;\n }\n\n this.reconnectInFlight = this.#runReconnectLoop(cause).finally(() => {\n this.reconnectInFlight = null;\n });\n }\n\n async #runReconnectLoop(initialCause: unknown): Promise<void> {\n let lastError: unknown = initialCause;\n\n for (let attempt = 1; attempt <= this.maxReconnectAttempts; attempt += 1) {\n if (this.closed || this.intentionalClose) {\n return;\n }\n\n this.onReconnect?.({ attempt, cause: lastError });\n\n const delay = this.reconnectDelayMs(attempt);\n if (delay > 0) {\n await new Promise<void>((resolve) => {\n setTimeout(resolve, delay);\n });\n }\n\n if (this.closed || this.intentionalClose) {\n return;\n }\n\n try {\n await this.open();\n if (this.onReconnected) {\n await this.onReconnected();\n }\n return;\n } catch (error) {\n lastError = error;\n }\n }\n\n this.queue.close(\n new MaxWebSocketReconnectAttemptsError(\n this.maxReconnectAttempts,\n lastError\n )\n );\n }\n}\n"],"mappings":"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"}
@@ -5,22 +5,50 @@ import { Command, CommandResponse, ErrorResponse, Message } from "@langchain/pro
5
5
  //#region src/client/stream/transport/websocket.d.ts
6
6
  /**
7
7
  * Transport adapter that speaks the thread-centric protocol over a
8
- * bidirectional WebSocket. Bound to a specific `threadId` the socket
9
- * connects to `ws://.../threads/:thread_id/stream/events`.
8
+ * bidirectional WebSocket. Bound to a `threadId` at construction or later
9
+ * via {@link setThreadId} — the socket connects to
10
+ * `ws://.../threads/:thread_id/stream/events`.
11
+ *
12
+ * On unexpected disconnect the adapter reconnects with exponential
13
+ * backoff (see {@link ProtocolWebSocketTransportOptions.maxReconnectAttempts}).
14
+ * The server replays buffered events on the new socket; the SDK
15
+ * deduplicates by `event_id`. {@link ProtocolWebSocketTransportOptions.onReconnected}
16
+ * runs after each successful reconnect so `ThreadStream` can re-issue
17
+ * `subscription.subscribe` commands.
10
18
  */
11
19
  declare class ProtocolWebSocketTransportAdapter implements TransportAdapter {
12
- readonly threadId: string;
20
+ #private;
21
+ threadId: string;
13
22
  private readonly queue;
14
23
  private readonly apiUrl;
15
24
  private readonly defaultHeaders?;
16
25
  private readonly onRequest?;
17
26
  private readonly webSocketFactory;
18
- private readonly streamUrl;
27
+ private readonly paths?;
28
+ private readonly maxReconnectAttempts;
29
+ private readonly onReconnect?;
30
+ private readonly reconnectDelayMs;
31
+ private onReconnected?;
19
32
  private readonly pending;
20
33
  private socket;
21
34
  private closed;
22
35
  private intentionalClose;
36
+ private reconnectInFlight;
23
37
  constructor(options: ProtocolWebSocketTransportOptions);
38
+ /** {@inheritDoc TransportAdapter.setThreadId} */
39
+ setThreadId(threadId: string): void;
40
+ /**
41
+ * Socket URL derives from the currently-bound thread so a single adapter
42
+ * can follow {@link setThreadId} re-binds; the next {@link open} connects
43
+ * to the new thread. A fixed `paths.stream` string overrides the default.
44
+ */
45
+ private get streamUrl();
46
+ /**
47
+ * Register a callback invoked after each successful reconnect. Used
48
+ * by {@link ThreadStream} to re-send active `subscription.subscribe`
49
+ * commands.
50
+ */
51
+ setOnReconnected(handler: () => void | Promise<void>): void;
24
52
  open(): Promise<void>;
25
53
  send(command: Command): Promise<CommandResponse | ErrorResponse | void>;
26
54
  events(): AsyncIterable<Message>;
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.cts","names":[],"sources":["../../../../src/client/stream/transport/websocket.ts"],"mappings":";;;;;;;AA4BA;;;cAAa,iCAAA,YAA6C,gBAAA;EAAA,SAC/C,QAAA;EAAA,iBAEQ,KAAA;EAAA,iBAEA,MAAA;EAAA,iBAEA,cAAA;EAAA,iBAEA,SAAA;EAAA,iBAEA,gBAAA;EAAA,iBAEA,SAAA;EAAA,iBAEA,OAAA;EAAA,QAET,MAAA;EAAA,QAEA,MAAA;EAAA,QAEA,gBAAA;EAER,WAAA,CAAY,OAAA,EAAS,iCAAA;EAWf,IAAA,CAAA,GAAQ,OAAA;EAkCR,IAAA,CACJ,OAAA,EAAS,OAAA,GACR,OAAA,CAAQ,eAAA,GAAkB,aAAA;EAI7B,MAAA,CAAA,GAAU,aAAA,CAAc,OAAA;EAalB,KAAA,CAAA,GAAS,OAAA;EAAA,QA2CP,gCAAA;EAAA,QAQM,WAAA;EAAA,iBAsBG,aAAA;EAAA,iBA0BA,WAAA;EAAA,iBAgBA,iBAAA;AAAA"}
1
+ {"version":3,"file":"websocket.d.cts","names":[],"sources":["../../../../src/client/stream/transport/websocket.ts"],"mappings":";;;;;;;;;;;;;;;;;;cAsEa,iCAAA,YAA6C,gBAAA;EAAA;EACxD,QAAA;EAAA,iBAEiB,KAAA;EAAA,iBAEA,MAAA;EAAA,iBAEA,cAAA;EAAA,iBAEA,SAAA;EAAA,iBAEA,gBAAA;EAAA,iBAEA,KAAA;EAAA,iBAEA,oBAAA;EAAA,iBAEA,WAAA;EAAA,iBAEA,gBAAA;EAAA,QAET,aAAA;EAAA,iBAES,OAAA;EAAA,QAET,MAAA;EAAA,QAEA,MAAA;EAAA,QAEA,gBAAA;EAAA,QAEA,iBAAA;EAER,WAAA,CAAY,OAAA,EAAS,iCAAA;EAoGG;EApFxB,WAAA,CAAY,QAAA;EAiGG;;;;;EAAA,YA7EH,SAAA,CAAA;EA2NsB;;;;;EA9MlC,gBAAA,CAAiB,OAAA,eAAsB,OAAA;EAIjC,IAAA,CAAA,GAAQ,OAAA;EAyCR,IAAA,CACJ,OAAA,EAAS,OAAA,GACR,OAAA,CAAQ,eAAA,GAAkB,aAAA;EAI7B,MAAA,CAAA,GAAU,aAAA,CAAc,OAAA;EAalB,KAAA,CAAA,GAAS,OAAA;EAAA,QA6CP,gCAAA;EAAA,QAQM,WAAA;EAAA,iBA8CG,aAAA;EAAA,iBA0BA,WAAA;EAAA,iBAiBA,iBAAA;AAAA"}
@@ -5,22 +5,50 @@ import { Command, CommandResponse, ErrorResponse, Message } from "@langchain/pro
5
5
  //#region src/client/stream/transport/websocket.d.ts
6
6
  /**
7
7
  * Transport adapter that speaks the thread-centric protocol over a
8
- * bidirectional WebSocket. Bound to a specific `threadId` the socket
9
- * connects to `ws://.../threads/:thread_id/stream/events`.
8
+ * bidirectional WebSocket. Bound to a `threadId` at construction or later
9
+ * via {@link setThreadId} — the socket connects to
10
+ * `ws://.../threads/:thread_id/stream/events`.
11
+ *
12
+ * On unexpected disconnect the adapter reconnects with exponential
13
+ * backoff (see {@link ProtocolWebSocketTransportOptions.maxReconnectAttempts}).
14
+ * The server replays buffered events on the new socket; the SDK
15
+ * deduplicates by `event_id`. {@link ProtocolWebSocketTransportOptions.onReconnected}
16
+ * runs after each successful reconnect so `ThreadStream` can re-issue
17
+ * `subscription.subscribe` commands.
10
18
  */
11
19
  declare class ProtocolWebSocketTransportAdapter implements TransportAdapter {
12
- readonly threadId: string;
20
+ #private;
21
+ threadId: string;
13
22
  private readonly queue;
14
23
  private readonly apiUrl;
15
24
  private readonly defaultHeaders?;
16
25
  private readonly onRequest?;
17
26
  private readonly webSocketFactory;
18
- private readonly streamUrl;
27
+ private readonly paths?;
28
+ private readonly maxReconnectAttempts;
29
+ private readonly onReconnect?;
30
+ private readonly reconnectDelayMs;
31
+ private onReconnected?;
19
32
  private readonly pending;
20
33
  private socket;
21
34
  private closed;
22
35
  private intentionalClose;
36
+ private reconnectInFlight;
23
37
  constructor(options: ProtocolWebSocketTransportOptions);
38
+ /** {@inheritDoc TransportAdapter.setThreadId} */
39
+ setThreadId(threadId: string): void;
40
+ /**
41
+ * Socket URL derives from the currently-bound thread so a single adapter
42
+ * can follow {@link setThreadId} re-binds; the next {@link open} connects
43
+ * to the new thread. A fixed `paths.stream` string overrides the default.
44
+ */
45
+ private get streamUrl();
46
+ /**
47
+ * Register a callback invoked after each successful reconnect. Used
48
+ * by {@link ThreadStream} to re-send active `subscription.subscribe`
49
+ * commands.
50
+ */
51
+ setOnReconnected(handler: () => void | Promise<void>): void;
24
52
  open(): Promise<void>;
25
53
  send(command: Command): Promise<CommandResponse | ErrorResponse | void>;
26
54
  events(): AsyncIterable<Message>;
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.ts","names":[],"sources":["../../../../src/client/stream/transport/websocket.ts"],"mappings":";;;;;;;AA4BA;;;cAAa,iCAAA,YAA6C,gBAAA;EAAA,SAC/C,QAAA;EAAA,iBAEQ,KAAA;EAAA,iBAEA,MAAA;EAAA,iBAEA,cAAA;EAAA,iBAEA,SAAA;EAAA,iBAEA,gBAAA;EAAA,iBAEA,SAAA;EAAA,iBAEA,OAAA;EAAA,QAET,MAAA;EAAA,QAEA,MAAA;EAAA,QAEA,gBAAA;EAER,WAAA,CAAY,OAAA,EAAS,iCAAA;EAWf,IAAA,CAAA,GAAQ,OAAA;EAkCR,IAAA,CACJ,OAAA,EAAS,OAAA,GACR,OAAA,CAAQ,eAAA,GAAkB,aAAA;EAI7B,MAAA,CAAA,GAAU,aAAA,CAAc,OAAA;EAalB,KAAA,CAAA,GAAS,OAAA;EAAA,QA2CP,gCAAA;EAAA,QAQM,WAAA;EAAA,iBAsBG,aAAA;EAAA,iBA0BA,WAAA;EAAA,iBAgBA,iBAAA;AAAA"}
1
+ {"version":3,"file":"websocket.d.ts","names":[],"sources":["../../../../src/client/stream/transport/websocket.ts"],"mappings":";;;;;;;;;;;;;;;;;;cAsEa,iCAAA,YAA6C,gBAAA;EAAA;EACxD,QAAA;EAAA,iBAEiB,KAAA;EAAA,iBAEA,MAAA;EAAA,iBAEA,cAAA;EAAA,iBAEA,SAAA;EAAA,iBAEA,gBAAA;EAAA,iBAEA,KAAA;EAAA,iBAEA,oBAAA;EAAA,iBAEA,WAAA;EAAA,iBAEA,gBAAA;EAAA,QAET,aAAA;EAAA,iBAES,OAAA;EAAA,QAET,MAAA;EAAA,QAEA,MAAA;EAAA,QAEA,gBAAA;EAAA,QAEA,iBAAA;EAER,WAAA,CAAY,OAAA,EAAS,iCAAA;EAoGG;EApFxB,WAAA,CAAY,QAAA;EAiGG;;;;;EAAA,YA7EH,SAAA,CAAA;EA2NsB;;;;;EA9MlC,gBAAA,CAAiB,OAAA,eAAsB,OAAA;EAIjC,IAAA,CAAA,GAAQ,OAAA;EAyCR,IAAA,CACJ,OAAA,EAAS,OAAA,GACR,OAAA,CAAQ,eAAA,GAAkB,aAAA;EAI7B,MAAA,CAAA,GAAU,aAAA,CAAc,OAAA;EAalB,KAAA,CAAA,GAAS,OAAA;EAAA,QA6CP,gCAAA;EAAA,QAQM,WAAA;EAAA,iBA8CG,aAAA;EAAA,iBA0BA,WAAA;EAAA,iBAiBA,iBAAA;AAAA"}
@@ -1,10 +1,29 @@
1
+ import { MaxWebSocketReconnectAttemptsError } from "../error.js";
1
2
  import { AsyncQueue } from "./queue.js";
2
- import { hasHeaders, isRecord, toAbsoluteUrl, toError, toWebSocketUrl } from "./utils.js";
3
+ import { hasHeaders, isRecord, resolveProtocolPath, toAbsoluteUrl, toError, toWebSocketUrl } from "./utils.js";
3
4
  //#region src/client/stream/transport/websocket.ts
5
+ const WEB_SOCKET_CONNECTING = 0;
6
+ const WEB_SOCKET_OPEN = 1;
7
+ const WEB_SOCKET_CLOSED = 3;
8
+ /**
9
+ * Exponential backoff with jitter for WebSocket reconnect. Mirrors
10
+ * {@link streamWithRetry} in `utils/stream.ts` (capped at 5s + 1s jitter).
11
+ */
12
+ function webSocketReconnectDelayMs(attempt) {
13
+ return Math.min(1e3 * 2 ** (attempt - 1), 5e3) + Math.random() * 1e3;
14
+ }
4
15
  /**
5
16
  * Transport adapter that speaks the thread-centric protocol over a
6
- * bidirectional WebSocket. Bound to a specific `threadId` the socket
7
- * connects to `ws://.../threads/:thread_id/stream/events`.
17
+ * bidirectional WebSocket. Bound to a `threadId` at construction or later
18
+ * via {@link setThreadId} — the socket connects to
19
+ * `ws://.../threads/:thread_id/stream/events`.
20
+ *
21
+ * On unexpected disconnect the adapter reconnects with exponential
22
+ * backoff (see {@link ProtocolWebSocketTransportOptions.maxReconnectAttempts}).
23
+ * The server replays buffered events on the new socket; the SDK
24
+ * deduplicates by `event_id`. {@link ProtocolWebSocketTransportOptions.onReconnected}
25
+ * runs after each successful reconnect so `ThreadStream` can re-issue
26
+ * `subscription.subscribe` commands.
8
27
  */
9
28
  var ProtocolWebSocketTransportAdapter = class {
10
29
  threadId;
@@ -13,30 +32,63 @@ var ProtocolWebSocketTransportAdapter = class {
13
32
  defaultHeaders;
14
33
  onRequest;
15
34
  webSocketFactory;
16
- streamUrl;
35
+ paths;
36
+ maxReconnectAttempts;
37
+ onReconnect;
38
+ reconnectDelayMs;
39
+ onReconnected;
17
40
  pending = /* @__PURE__ */ new Map();
18
41
  socket = null;
19
42
  closed = false;
20
43
  intentionalClose = false;
44
+ reconnectInFlight = null;
21
45
  constructor(options) {
22
46
  this.apiUrl = options.apiUrl;
23
- this.threadId = options.threadId;
47
+ this.threadId = options.threadId ?? "";
24
48
  this.defaultHeaders = options.defaultHeaders;
25
49
  this.onRequest = options.onRequest;
26
50
  this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
27
- this.streamUrl = options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;
51
+ this.paths = options.paths;
52
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
53
+ this.onReconnect = options.onReconnect;
54
+ this.onReconnected = options.onReconnected;
55
+ this.reconnectDelayMs = options.reconnectDelayMs ?? webSocketReconnectDelayMs;
56
+ }
57
+ /** {@inheritDoc TransportAdapter.setThreadId} */
58
+ setThreadId(threadId) {
59
+ if (threadId === this.threadId) return;
60
+ if (this.reconnectInFlight != null || this.socket != null && this.socket.readyState !== WEB_SOCKET_CLOSED) throw new Error("Protocol WebSocket transport cannot be rebound to a different thread while the socket is open. Close the current stream and create a new WebSocket transport for the new thread.");
61
+ this.threadId = threadId;
62
+ }
63
+ /**
64
+ * Socket URL derives from the currently-bound thread so a single adapter
65
+ * can follow {@link setThreadId} re-binds; the next {@link open} connects
66
+ * to the new thread. A fixed `paths.stream` string overrides the default.
67
+ */
68
+ get streamUrl() {
69
+ return resolveProtocolPath(this.paths?.stream, this.threadId, (id) => `/threads/${id}/stream/events`);
70
+ }
71
+ /**
72
+ * Register a callback invoked after each successful reconnect. Used
73
+ * by {@link ThreadStream} to re-send active `subscription.subscribe`
74
+ * commands.
75
+ */
76
+ setOnReconnected(handler) {
77
+ this.onReconnected = handler;
28
78
  }
29
79
  async open() {
30
- if (this.socket != null) return;
80
+ if (this.closed) throw new Error("Protocol WebSocket transport is closed.");
81
+ if (this.socket?.readyState === WEB_SOCKET_OPEN) return;
82
+ if (this.socket != null) {
83
+ this.#detachSocket(this.socket);
84
+ this.socket = null;
85
+ }
31
86
  this.assertBrowserSafeTransportConfig();
32
87
  const wsUrl = toWebSocketUrl(toAbsoluteUrl(this.apiUrl, this.streamUrl).toString());
33
88
  const socket = this.webSocketFactory(wsUrl);
34
89
  this.socket = socket;
35
- this.closed = false;
36
90
  this.intentionalClose = false;
37
- socket.addEventListener("message", this.handleMessage);
38
- socket.addEventListener("close", this.handleClose);
39
- socket.addEventListener("error", this.handleSocketError);
91
+ this.#attachSocket(socket);
40
92
  await new Promise((resolve, reject) => {
41
93
  const onOpen = () => {
42
94
  cleanup();
@@ -80,8 +132,9 @@ var ProtocolWebSocketTransportAdapter = class {
80
132
  const socket = this.socket;
81
133
  this.socket = null;
82
134
  if (!socket) return;
135
+ this.#detachSocket(socket);
83
136
  await new Promise((resolve) => {
84
- if (socket.readyState === WebSocket.CLOSED) {
137
+ if (socket.readyState === WEB_SOCKET_CLOSED) {
85
138
  resolve();
86
139
  return;
87
140
  }
@@ -90,7 +143,7 @@ var ProtocolWebSocketTransportAdapter = class {
90
143
  resolve();
91
144
  };
92
145
  socket.addEventListener("close", onClose, { once: true });
93
- if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close();
146
+ if (socket.readyState === WEB_SOCKET_OPEN || socket.readyState === WEB_SOCKET_CONNECTING) socket.close();
94
147
  else resolve();
95
148
  });
96
149
  }
@@ -98,8 +151,12 @@ var ProtocolWebSocketTransportAdapter = class {
98
151
  if (hasHeaders(this.defaultHeaders) || this.onRequest != null) throw new Error("Browser WebSocket protocol transport does not support defaultHeaders or onRequest hooks. Supply a custom protocolWebSocketFactory if you need custom WebSocket setup.");
99
152
  }
100
153
  async sendCommand(command) {
101
- const socket = this.socket;
102
- if (socket == null || socket.readyState !== WebSocket.OPEN) throw new Error("Protocol WebSocket is not open.");
154
+ let socket = this.socket;
155
+ if (this.reconnectInFlight != null && (socket == null || socket.readyState !== WEB_SOCKET_OPEN)) {
156
+ await this.reconnectInFlight.catch(() => void 0);
157
+ socket = this.socket;
158
+ }
159
+ if (socket == null || socket.readyState !== WEB_SOCKET_OPEN) throw new Error("Protocol WebSocket is not open.");
103
160
  return await new Promise((resolve, reject) => {
104
161
  this.pending.set(command.id, {
105
162
  resolve,
@@ -113,6 +170,16 @@ var ProtocolWebSocketTransportAdapter = class {
113
170
  }
114
171
  });
115
172
  }
173
+ #attachSocket(socket) {
174
+ socket.addEventListener("message", this.handleMessage);
175
+ socket.addEventListener("close", this.handleClose);
176
+ socket.addEventListener("error", this.handleSocketError);
177
+ }
178
+ #detachSocket(socket) {
179
+ socket.removeEventListener("message", this.handleMessage);
180
+ socket.removeEventListener("close", this.handleClose);
181
+ socket.removeEventListener("error", this.handleSocketError);
182
+ }
116
183
  handleMessage = (event) => {
117
184
  let payload;
118
185
  try {
@@ -131,25 +198,61 @@ var ProtocolWebSocketTransportAdapter = class {
131
198
  if (isRecord(payload) && payload.type === "event") this.queue.push(payload);
132
199
  };
133
200
  handleClose = () => {
201
+ const socket = this.socket;
202
+ if (socket != null) this.#detachSocket(socket);
134
203
  this.socket = null;
135
204
  if (this.intentionalClose || this.closed) {
136
205
  this.queue.close();
137
206
  return;
138
207
  }
139
- const error = /* @__PURE__ */ new Error("Protocol WebSocket closed unexpectedly.");
140
- for (const { reject } of this.pending.values()) reject(error);
141
- this.pending.clear();
142
- this.queue.close(error);
208
+ this.#handleUnexpectedDisconnect(/* @__PURE__ */ new Error("Protocol WebSocket closed unexpectedly."));
143
209
  };
144
210
  handleSocketError = () => {
145
211
  if (this.closed || this.intentionalClose) return;
146
- const error = /* @__PURE__ */ new Error("Protocol WebSocket encountered an error.");
212
+ this.#handleUnexpectedDisconnect(/* @__PURE__ */ new Error("Protocol WebSocket encountered an error."));
213
+ };
214
+ #handleUnexpectedDisconnect(cause) {
215
+ const error = toError(cause);
147
216
  for (const { reject } of this.pending.values()) reject(error);
148
217
  this.pending.clear();
149
- this.queue.close(error);
150
- };
218
+ if (this.maxReconnectAttempts <= 0) {
219
+ this.queue.close(error);
220
+ return;
221
+ }
222
+ this.#scheduleReconnect(cause);
223
+ }
224
+ #scheduleReconnect(cause) {
225
+ if (this.closed || this.intentionalClose) return;
226
+ if (this.reconnectInFlight != null) return;
227
+ this.reconnectInFlight = this.#runReconnectLoop(cause).finally(() => {
228
+ this.reconnectInFlight = null;
229
+ });
230
+ }
231
+ async #runReconnectLoop(initialCause) {
232
+ let lastError = initialCause;
233
+ for (let attempt = 1; attempt <= this.maxReconnectAttempts; attempt += 1) {
234
+ if (this.closed || this.intentionalClose) return;
235
+ this.onReconnect?.({
236
+ attempt,
237
+ cause: lastError
238
+ });
239
+ const delay = this.reconnectDelayMs(attempt);
240
+ if (delay > 0) await new Promise((resolve) => {
241
+ setTimeout(resolve, delay);
242
+ });
243
+ if (this.closed || this.intentionalClose) return;
244
+ try {
245
+ await this.open();
246
+ if (this.onReconnected) await this.onReconnected();
247
+ return;
248
+ } catch (error) {
249
+ lastError = error;
250
+ }
251
+ }
252
+ this.queue.close(new MaxWebSocketReconnectAttemptsError(this.maxReconnectAttempts, lastError));
253
+ }
151
254
  };
152
255
  //#endregion
153
- export { ProtocolWebSocketTransportAdapter };
256
+ export { ProtocolWebSocketTransportAdapter, webSocketReconnectDelayMs };
154
257
 
155
258
  //# sourceMappingURL=websocket.js.map