@gravity-rail/sdk 0.36.0 → 0.40.0

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 (57) hide show
  1. package/README.md +44 -21
  2. package/dist/agui.d.cts +1 -1
  3. package/dist/agui.d.ts +1 -1
  4. package/dist/audio.d.cts +1 -1
  5. package/dist/audio.d.ts +1 -1
  6. package/dist/{chat-interaction-Ck1oMOr5.d.ts → chat-interaction-BB3ap8_u.d.ts} +8920 -6212
  7. package/dist/{chat-interaction-DqQu8Ad1.d.cts → chat-interaction-Bj1pciwo.d.cts} +8920 -6212
  8. package/dist/{chunk-P2E6LU7I.js → chunk-67QXTQVL.js} +1 -26
  9. package/dist/chunk-67QXTQVL.js.map +1 -0
  10. package/dist/chunk-6KPGCPGW.js +48 -0
  11. package/dist/chunk-6KPGCPGW.js.map +1 -0
  12. package/dist/{chunk-HRUMZ46V.js → chunk-72SWNLVD.js} +2 -1
  13. package/dist/chunk-72SWNLVD.js.map +1 -0
  14. package/dist/{chunk-R3C63RZJ.js → chunk-ZTUOB4V2.js} +1278 -218
  15. package/dist/chunk-ZTUOB4V2.js.map +1 -0
  16. package/dist/helpers.cjs +47 -0
  17. package/dist/helpers.cjs.map +1 -1
  18. package/dist/helpers.d.cts +4 -3
  19. package/dist/helpers.d.ts +4 -3
  20. package/dist/helpers.js +7 -1
  21. package/dist/helpers.js.map +1 -1
  22. package/dist/index.cjs +1389 -350
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +13 -35
  25. package/dist/index.d.ts +13 -35
  26. package/dist/index.js +112 -122
  27. package/dist/index.js.map +1 -1
  28. package/dist/internal.cjs +1303 -252
  29. package/dist/internal.cjs.map +1 -1
  30. package/dist/internal.d.cts +338 -55
  31. package/dist/internal.d.ts +338 -55
  32. package/dist/internal.js +68 -21
  33. package/dist/internal.js.map +1 -1
  34. package/dist/member-import-D8fZr2Pd.d.cts +46 -0
  35. package/dist/member-import-YNk9MlEl.d.ts +46 -0
  36. package/dist/realtime.cjs +91 -20
  37. package/dist/realtime.cjs.map +1 -1
  38. package/dist/realtime.d.cts +34 -9
  39. package/dist/realtime.d.ts +34 -9
  40. package/dist/realtime.js +90 -20
  41. package/dist/realtime.js.map +1 -1
  42. package/dist/{revision-state-Tn2V0-zt.d.cts → revision-state-BaJTq3Or.d.ts} +271 -55
  43. package/dist/{revision-state-V5RIhte0.d.ts → revision-state-CL11FA-T.d.cts} +271 -55
  44. package/dist/schemas.cjs +58 -81
  45. package/dist/schemas.cjs.map +1 -1
  46. package/dist/schemas.d.cts +88 -225
  47. package/dist/schemas.d.ts +88 -225
  48. package/dist/schemas.js +58 -76
  49. package/dist/schemas.js.map +1 -1
  50. package/dist/{tasks-BH_i_pVb.d.cts → tasks-ByJCQ5BI.d.cts} +3003 -2508
  51. package/dist/{tasks-BH_i_pVb.d.ts → tasks-ByJCQ5BI.d.ts} +3003 -2508
  52. package/dist/{types-0c6_NGU4.d.cts → types-DlfADjPR.d.cts} +17 -2
  53. package/dist/{types-0c6_NGU4.d.ts → types-DlfADjPR.d.ts} +17 -2
  54. package/package.json +9 -7
  55. package/dist/chunk-HRUMZ46V.js.map +0 -1
  56. package/dist/chunk-P2E6LU7I.js.map +0 -1
  57. package/dist/chunk-R3C63RZJ.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { R as RealtimeSessionBase, a as ChannelListener, U as Unsubscribe, b as RealtimeAuthSession, A as AccountRealtimeSession, O as OperatorSessionState, c as OperatorCallPhase, C as ChannelEvent } from './types-0c6_NGU4.cjs';
2
- export { d as ACCOUNT_ACTIVITY_CHANNEL, e as ACCOUNT_PRESENCE_CHANNEL, f as ACTIVITY_CHANNEL, g as ACTIVITY_MEMBER_CHANNEL, h as ActivityChannelName, i as ActivityInvalidationEvent, P as PRESENCE_CHANNEL, j as isActiveCallPhase, k as isActivityInvalidation } from './types-0c6_NGU4.cjs';
1
+ import { R as RealtimeSessionBase, a as ChannelListener, U as Unsubscribe, b as RealtimeAuthSession, A as AccountRealtimeSession, O as OperatorSessionState, c as OperatorCallPhase, C as ChannelEvent } from './types-DlfADjPR.cjs';
2
+ export { d as ACCOUNT_ACTIVITY_CHANNEL, e as ACCOUNT_PRESENCE_CHANNEL, f as ACTIVITY_CHANNEL, g as ACTIVITY_MEMBER_CHANNEL, h as ActivityChannelName, i as ActivityInvalidationEvent, P as PRESENCE_CHANNEL, j as RealtimeLoginTokenProvider, k as isActiveCallPhase, l as isActivityInvalidation } from './types-DlfADjPR.cjs';
3
3
 
4
4
  /**
5
5
  * The realtime socket state machine, shared by both mounts.
@@ -47,9 +47,9 @@ declare abstract class RealtimeSocketClient<TSession extends RealtimeSessionBase
47
47
  /**
48
48
  * Whether replacing `previous` with `next` invalidates the open socket.
49
49
  *
50
- * The token always does; the rest is mount-specific — a workspace client
51
- * changing workspace is a different socket, and an account client has no
52
- * such axis.
50
+ * A different login-token provider always does — it stands for a different
51
+ * identity; the rest is mount-specific — a workspace client changing
52
+ * workspace is a different socket, and an account client has no such axis.
53
53
  */
54
54
  protected sessionRequiresReconnect(previous: TSession | null, next: TSession): boolean;
55
55
  protected resolveSession(): TSession | null;
@@ -93,13 +93,29 @@ declare abstract class RealtimeSocketClient<TSession extends RealtimeSessionBase
93
93
  /** The last failure the socket gave up on, or null. See {@link SocketState.lastError}. */
94
94
  getLastError(): string | null;
95
95
  protected ensureConnection(): void;
96
+ /**
97
+ * Mint this attempt's login token, then open the socket with it.
98
+ *
99
+ * A fresh token per attempt, reconnects included: a browser's token is
100
+ * single-use and expires in seconds, so a token from an earlier attempt is
101
+ * already spent.
102
+ */
103
+ private openSocket;
104
+ /**
105
+ * End an attempt that never opened a socket. No `onclose` will run for it,
106
+ * so the retry decision is made here, on the normal backoff.
107
+ */
108
+ private releaseUnopenedAttempt;
109
+ /** Decide whether a connection that just ended (or never opened) retries. */
110
+ private afterConnectionEnded;
96
111
  protected closeConnection(): void;
97
112
  }
98
113
 
99
114
  /**
100
115
  * Unified workspace WebSocket client for real-time events.
101
116
  *
102
- * Mirrors `/api/v2/w/{wid}/ws/realtime`: login with OAuth access token,
117
+ * Mirrors `/api/v2/w/{wid}/ws/realtime`: log in with a token from the session's
118
+ * `getLoginToken` provider (fresh per connect attempt),
103
119
  * subscribe to channels, receive AGUI events.
104
120
  *
105
121
  * The socket state machine — login, heartbeat, reconnect, subscribe retry —
@@ -136,7 +152,7 @@ declare function isWorkspaceSocketConnected(): boolean;
136
152
  * Account WebSocket client — one socket per tab, rooted at the Account.
137
153
  *
138
154
  * Mirrors `/api/v2/ws/realtime`. No workspace in the URL and none in the
139
- * session: the token is the whole identity, and what the socket may see comes
155
+ * session: the login token is the whole identity, and what the socket may see comes
140
156
  * from the server's grant map, refreshed there.
141
157
  *
142
158
  * See `docs/projects/active/account-socket.md`. The point is not a second
@@ -148,7 +164,7 @@ declare function isWorkspaceSocketConnected(): boolean;
148
164
  * deleted at cutover rather than lived alongside.
149
165
  *
150
166
  * ```ts
151
- * const client = new AccountRealtimeClient({ apiUrl, accessToken });
167
+ * const client = new AccountRealtimeClient({ apiUrl, getLoginToken });
152
168
  * client.subscribe(workspaceChannel(wid, 'activity'), onInvalidate);
153
169
  * client.subscribe(ACCOUNT_ACTIVITY_CHANNEL, onInvalidate);
154
170
  * ```
@@ -188,6 +204,13 @@ declare function subscribeAccountChannel(channel: string, listener: ChannelListe
188
204
  declare function onAccountSocketConnection(listener: (connected: boolean) => void): () => void;
189
205
  declare function isAccountSocketConnected(): boolean;
190
206
 
207
+ /**
208
+ * How long one connect attempt waits for its login token. A provider that
209
+ * never settles (a hung mint request) must not hold the attempt open forever:
210
+ * past this the attempt fails like any other and the caller's backoff applies.
211
+ */
212
+ declare const LOGIN_TOKEN_TIMEOUT_MS = 10000;
213
+
191
214
  /**
192
215
  * Human operator session WebSocket client.
193
216
  *
@@ -220,6 +243,8 @@ declare class OperatorSessionClient {
220
243
  private heartbeatTimer;
221
244
  private sessionCloseNotified;
222
245
  private intentionalDisconnect;
246
+ /** Bumped by every connect and disconnect; a stale connect stops at its next await. */
247
+ private connectGeneration;
223
248
  constructor(handlers?: OperatorSessionHandlers);
224
249
  getSessionState(): OperatorSessionState;
225
250
  getCallPhase(): OperatorCallPhase;
@@ -269,4 +294,4 @@ declare class OperatorSessionClient {
269
294
  disconnect(): Promise<void>;
270
295
  }
271
296
 
272
- export { AccountRealtimeClient, AccountRealtimeSession, ChannelEvent, ChannelListener, OperatorCallPhase, OperatorSessionClient, type OperatorSessionHandlers, OperatorSessionState, type ProtocolInitMessage, RealtimeAuthSession, RealtimeSessionBase, RealtimeSocketClient, Unsubscribe, WorkspaceRealtimeClient, buildAccountWsUrl, buildWorkspaceWsUrl, initAccountSocket, initWorkspaceSocket, isAccountSocketConnected, isWorkspaceSocketConnected, onAccountSocketConnection, onWorkspaceSocketConnection, subscribeAccountChannel, subscribeWorkspaceChannel, workspaceChannel };
297
+ export { AccountRealtimeClient, AccountRealtimeSession, ChannelEvent, ChannelListener, LOGIN_TOKEN_TIMEOUT_MS, OperatorCallPhase, OperatorSessionClient, type OperatorSessionHandlers, OperatorSessionState, type ProtocolInitMessage, RealtimeAuthSession, RealtimeSessionBase, RealtimeSocketClient, Unsubscribe, WorkspaceRealtimeClient, buildAccountWsUrl, buildWorkspaceWsUrl, initAccountSocket, initWorkspaceSocket, isAccountSocketConnected, isWorkspaceSocketConnected, onAccountSocketConnection, onWorkspaceSocketConnection, subscribeAccountChannel, subscribeWorkspaceChannel, workspaceChannel };
@@ -1,5 +1,5 @@
1
- import { R as RealtimeSessionBase, a as ChannelListener, U as Unsubscribe, b as RealtimeAuthSession, A as AccountRealtimeSession, O as OperatorSessionState, c as OperatorCallPhase, C as ChannelEvent } from './types-0c6_NGU4.js';
2
- export { d as ACCOUNT_ACTIVITY_CHANNEL, e as ACCOUNT_PRESENCE_CHANNEL, f as ACTIVITY_CHANNEL, g as ACTIVITY_MEMBER_CHANNEL, h as ActivityChannelName, i as ActivityInvalidationEvent, P as PRESENCE_CHANNEL, j as isActiveCallPhase, k as isActivityInvalidation } from './types-0c6_NGU4.js';
1
+ import { R as RealtimeSessionBase, a as ChannelListener, U as Unsubscribe, b as RealtimeAuthSession, A as AccountRealtimeSession, O as OperatorSessionState, c as OperatorCallPhase, C as ChannelEvent } from './types-DlfADjPR.js';
2
+ export { d as ACCOUNT_ACTIVITY_CHANNEL, e as ACCOUNT_PRESENCE_CHANNEL, f as ACTIVITY_CHANNEL, g as ACTIVITY_MEMBER_CHANNEL, h as ActivityChannelName, i as ActivityInvalidationEvent, P as PRESENCE_CHANNEL, j as RealtimeLoginTokenProvider, k as isActiveCallPhase, l as isActivityInvalidation } from './types-DlfADjPR.js';
3
3
 
4
4
  /**
5
5
  * The realtime socket state machine, shared by both mounts.
@@ -47,9 +47,9 @@ declare abstract class RealtimeSocketClient<TSession extends RealtimeSessionBase
47
47
  /**
48
48
  * Whether replacing `previous` with `next` invalidates the open socket.
49
49
  *
50
- * The token always does; the rest is mount-specific — a workspace client
51
- * changing workspace is a different socket, and an account client has no
52
- * such axis.
50
+ * A different login-token provider always does — it stands for a different
51
+ * identity; the rest is mount-specific — a workspace client changing
52
+ * workspace is a different socket, and an account client has no such axis.
53
53
  */
54
54
  protected sessionRequiresReconnect(previous: TSession | null, next: TSession): boolean;
55
55
  protected resolveSession(): TSession | null;
@@ -93,13 +93,29 @@ declare abstract class RealtimeSocketClient<TSession extends RealtimeSessionBase
93
93
  /** The last failure the socket gave up on, or null. See {@link SocketState.lastError}. */
94
94
  getLastError(): string | null;
95
95
  protected ensureConnection(): void;
96
+ /**
97
+ * Mint this attempt's login token, then open the socket with it.
98
+ *
99
+ * A fresh token per attempt, reconnects included: a browser's token is
100
+ * single-use and expires in seconds, so a token from an earlier attempt is
101
+ * already spent.
102
+ */
103
+ private openSocket;
104
+ /**
105
+ * End an attempt that never opened a socket. No `onclose` will run for it,
106
+ * so the retry decision is made here, on the normal backoff.
107
+ */
108
+ private releaseUnopenedAttempt;
109
+ /** Decide whether a connection that just ended (or never opened) retries. */
110
+ private afterConnectionEnded;
96
111
  protected closeConnection(): void;
97
112
  }
98
113
 
99
114
  /**
100
115
  * Unified workspace WebSocket client for real-time events.
101
116
  *
102
- * Mirrors `/api/v2/w/{wid}/ws/realtime`: login with OAuth access token,
117
+ * Mirrors `/api/v2/w/{wid}/ws/realtime`: log in with a token from the session's
118
+ * `getLoginToken` provider (fresh per connect attempt),
103
119
  * subscribe to channels, receive AGUI events.
104
120
  *
105
121
  * The socket state machine — login, heartbeat, reconnect, subscribe retry —
@@ -136,7 +152,7 @@ declare function isWorkspaceSocketConnected(): boolean;
136
152
  * Account WebSocket client — one socket per tab, rooted at the Account.
137
153
  *
138
154
  * Mirrors `/api/v2/ws/realtime`. No workspace in the URL and none in the
139
- * session: the token is the whole identity, and what the socket may see comes
155
+ * session: the login token is the whole identity, and what the socket may see comes
140
156
  * from the server's grant map, refreshed there.
141
157
  *
142
158
  * See `docs/projects/active/account-socket.md`. The point is not a second
@@ -148,7 +164,7 @@ declare function isWorkspaceSocketConnected(): boolean;
148
164
  * deleted at cutover rather than lived alongside.
149
165
  *
150
166
  * ```ts
151
- * const client = new AccountRealtimeClient({ apiUrl, accessToken });
167
+ * const client = new AccountRealtimeClient({ apiUrl, getLoginToken });
152
168
  * client.subscribe(workspaceChannel(wid, 'activity'), onInvalidate);
153
169
  * client.subscribe(ACCOUNT_ACTIVITY_CHANNEL, onInvalidate);
154
170
  * ```
@@ -188,6 +204,13 @@ declare function subscribeAccountChannel(channel: string, listener: ChannelListe
188
204
  declare function onAccountSocketConnection(listener: (connected: boolean) => void): () => void;
189
205
  declare function isAccountSocketConnected(): boolean;
190
206
 
207
+ /**
208
+ * How long one connect attempt waits for its login token. A provider that
209
+ * never settles (a hung mint request) must not hold the attempt open forever:
210
+ * past this the attempt fails like any other and the caller's backoff applies.
211
+ */
212
+ declare const LOGIN_TOKEN_TIMEOUT_MS = 10000;
213
+
191
214
  /**
192
215
  * Human operator session WebSocket client.
193
216
  *
@@ -220,6 +243,8 @@ declare class OperatorSessionClient {
220
243
  private heartbeatTimer;
221
244
  private sessionCloseNotified;
222
245
  private intentionalDisconnect;
246
+ /** Bumped by every connect and disconnect; a stale connect stops at its next await. */
247
+ private connectGeneration;
223
248
  constructor(handlers?: OperatorSessionHandlers);
224
249
  getSessionState(): OperatorSessionState;
225
250
  getCallPhase(): OperatorCallPhase;
@@ -269,4 +294,4 @@ declare class OperatorSessionClient {
269
294
  disconnect(): Promise<void>;
270
295
  }
271
296
 
272
- export { AccountRealtimeClient, AccountRealtimeSession, ChannelEvent, ChannelListener, OperatorCallPhase, OperatorSessionClient, type OperatorSessionHandlers, OperatorSessionState, type ProtocolInitMessage, RealtimeAuthSession, RealtimeSessionBase, RealtimeSocketClient, Unsubscribe, WorkspaceRealtimeClient, buildAccountWsUrl, buildWorkspaceWsUrl, initAccountSocket, initWorkspaceSocket, isAccountSocketConnected, isWorkspaceSocketConnected, onAccountSocketConnection, onWorkspaceSocketConnection, subscribeAccountChannel, subscribeWorkspaceChannel, workspaceChannel };
297
+ export { AccountRealtimeClient, AccountRealtimeSession, ChannelEvent, ChannelListener, LOGIN_TOKEN_TIMEOUT_MS, OperatorCallPhase, OperatorSessionClient, type OperatorSessionHandlers, OperatorSessionState, type ProtocolInitMessage, RealtimeAuthSession, RealtimeSessionBase, RealtimeSocketClient, Unsubscribe, WorkspaceRealtimeClient, buildAccountWsUrl, buildWorkspaceWsUrl, initAccountSocket, initWorkspaceSocket, isAccountSocketConnected, isWorkspaceSocketConnected, onAccountSocketConnection, onWorkspaceSocketConnection, subscribeAccountChannel, subscribeWorkspaceChannel, workspaceChannel };
package/dist/realtime.js CHANGED
@@ -22,6 +22,25 @@ function isActivityInvalidation(event) {
22
22
  return scope === "workspace" || scope === "member" || scope === "account";
23
23
  }
24
24
 
25
+ // src/realtime/login-token.ts
26
+ var LOGIN_TOKEN_TIMEOUT_MS = 1e4;
27
+ async function mintLoginToken(provider) {
28
+ let timer;
29
+ try {
30
+ const token = await Promise.race([
31
+ provider(),
32
+ new Promise((resolve) => {
33
+ timer = setTimeout(() => resolve(null), LOGIN_TOKEN_TIMEOUT_MS);
34
+ })
35
+ ]);
36
+ return typeof token === "string" && token.length > 0 ? token : null;
37
+ } catch {
38
+ return null;
39
+ } finally {
40
+ if (timer !== void 0) clearTimeout(timer);
41
+ }
42
+ }
43
+
25
44
  // src/realtime/socket-client.ts
26
45
  var HEARTBEAT_INTERVAL_MS = 3e4;
27
46
  var RECONNECT_DELAY_MS = 3e3;
@@ -44,7 +63,8 @@ function createSocketState() {
44
63
  reconnectTimer: null,
45
64
  heartbeatTimer: null,
46
65
  closeTimer: null,
47
- intentionalClose: false
66
+ intentionalClose: false,
67
+ connectGeneration: 0
48
68
  };
49
69
  }
50
70
  var RealtimeSocketClient = class {
@@ -65,12 +85,12 @@ var RealtimeSocketClient = class {
65
85
  /**
66
86
  * Whether replacing `previous` with `next` invalidates the open socket.
67
87
  *
68
- * The token always does; the rest is mount-specific — a workspace client
69
- * changing workspace is a different socket, and an account client has no
70
- * such axis.
88
+ * A different login-token provider always does — it stands for a different
89
+ * identity; the rest is mount-specific — a workspace client changing
90
+ * workspace is a different socket, and an account client has no such axis.
71
91
  */
72
92
  sessionRequiresReconnect(previous, next) {
73
- return previous?.accessToken !== next.accessToken || previous?.apiUrl !== next.apiUrl;
93
+ return previous?.getLoginToken !== next.getLoginToken || previous?.apiUrl !== next.apiUrl;
74
94
  }
75
95
  resolveSession() {
76
96
  return this.session ?? this.getSessionFn?.() ?? null;
@@ -135,7 +155,7 @@ var RealtimeSocketClient = class {
135
155
  setSession(session) {
136
156
  const changed = this.sessionRequiresReconnect(this.session, session);
137
157
  this.session = session;
138
- if (changed && this.state.ws) {
158
+ if (changed && (this.state.ws || this.state.connecting)) {
139
159
  this.closeConnection();
140
160
  }
141
161
  if (this.state.listeners.size > 0) {
@@ -234,10 +254,33 @@ var RealtimeSocketClient = class {
234
254
  this.clearCloseTimer();
235
255
  this.state.connecting = true;
236
256
  this.state.intentionalClose = false;
257
+ const generation = ++this.state.connectGeneration;
237
258
  this.notifyConnectionListeners();
238
- const ws = new WebSocket(this.buildUrl(session));
259
+ void this.openSocket(session, generation);
260
+ }
261
+ /**
262
+ * Mint this attempt's login token, then open the socket with it.
263
+ *
264
+ * A fresh token per attempt, reconnects included: a browser's token is
265
+ * single-use and expires in seconds, so a token from an earlier attempt is
266
+ * already spent.
267
+ */
268
+ async openSocket(session, generation) {
269
+ const token = await mintLoginToken(session.getLoginToken);
270
+ if (generation !== this.state.connectGeneration) return;
271
+ if (!token) {
272
+ this.releaseUnopenedAttempt();
273
+ return;
274
+ }
275
+ let ws;
276
+ try {
277
+ ws = new WebSocket(this.buildUrl(session));
278
+ } catch {
279
+ this.releaseUnopenedAttempt();
280
+ return;
281
+ }
239
282
  ws.onopen = () => {
240
- ws.send(JSON.stringify({ type: "login", token: session.accessToken }));
283
+ ws.send(JSON.stringify({ type: "login", token }));
241
284
  };
242
285
  ws.onmessage = (event) => {
243
286
  if (this.state.ws !== null && this.state.ws !== ws) return;
@@ -331,24 +374,37 @@ var RealtimeSocketClient = class {
331
374
  clearInterval(this.state.heartbeatTimer);
332
375
  this.state.heartbeatTimer = null;
333
376
  }
334
- const willRetry = !this.state.intentionalClose && this.state.listeners.size > 0 && this.state.reconnectAttempts < MAX_RECONNECT_ATTEMPTS;
335
- if (!willRetry && !this.state.intentionalClose && this.state.listeners.size > 0) {
336
- this.state.lastError = "Realtime connection lost";
337
- }
338
- this.notifyConnectionListeners();
339
- if (willRetry) {
340
- this.state.reconnectAttempts += 1;
341
- this.state.reconnectTimer = setTimeout(() => {
342
- this.ensureConnection();
343
- }, RECONNECT_DELAY_MS * this.state.reconnectAttempts);
344
- }
377
+ this.afterConnectionEnded();
345
378
  };
346
379
  ws.onerror = () => {
347
380
  };
348
381
  this.state.ws = ws;
349
382
  }
383
+ /**
384
+ * End an attempt that never opened a socket. No `onclose` will run for it,
385
+ * so the retry decision is made here, on the normal backoff.
386
+ */
387
+ releaseUnopenedAttempt() {
388
+ this.state.connecting = false;
389
+ this.afterConnectionEnded();
390
+ }
391
+ /** Decide whether a connection that just ended (or never opened) retries. */
392
+ afterConnectionEnded() {
393
+ const willRetry = !this.state.intentionalClose && this.state.listeners.size > 0 && this.state.reconnectAttempts < MAX_RECONNECT_ATTEMPTS;
394
+ if (!willRetry && !this.state.intentionalClose && this.state.listeners.size > 0) {
395
+ this.state.lastError = "Realtime connection lost";
396
+ }
397
+ this.notifyConnectionListeners();
398
+ if (willRetry) {
399
+ this.state.reconnectAttempts += 1;
400
+ this.state.reconnectTimer = setTimeout(() => {
401
+ this.ensureConnection();
402
+ }, RECONNECT_DELAY_MS * this.state.reconnectAttempts);
403
+ }
404
+ }
350
405
  closeConnection() {
351
406
  this.state.intentionalClose = true;
407
+ this.state.connectGeneration += 1;
352
408
  this.clearCloseTimer();
353
409
  if (this.state.reconnectTimer) {
354
410
  clearTimeout(this.state.reconnectTimer);
@@ -490,6 +546,8 @@ var OperatorSessionClient = class {
490
546
  heartbeatTimer = null;
491
547
  sessionCloseNotified = false;
492
548
  intentionalDisconnect = false;
549
+ /** Bumped by every connect and disconnect; a stale connect stops at its next await. */
550
+ connectGeneration = 0;
493
551
  getSessionState() {
494
552
  return this.sessionState;
495
553
  }
@@ -514,17 +572,27 @@ var OperatorSessionClient = class {
514
572
  throw new Error("Operator session WebSocket is not available in this environment");
515
573
  }
516
574
  await this.disconnect();
575
+ const generation = ++this.connectGeneration;
517
576
  this.sessionCloseNotified = false;
518
577
  this.setSessionState("connecting");
519
578
  this.setCallPhase("connecting");
520
579
  const isSsl = session.apiUrl.startsWith("https");
521
580
  const host = session.apiUrl.replace(/^https?:\/\//, "");
522
581
  const wsUrl = `${isSsl ? "wss" : "ws"}://${host}/api/v2/w/${session.workspaceUuid}/operator/chats/${chatId}/session-ws`;
582
+ const token = await mintLoginToken(session.getLoginToken);
583
+ if (generation !== this.connectGeneration) {
584
+ throw new Error("Operator session connect superseded");
585
+ }
586
+ if (!token) {
587
+ this.setSessionState("error");
588
+ this.setCallPhase("idle");
589
+ throw new Error("Realtime login token unavailable");
590
+ }
523
591
  await new Promise((resolve, reject) => {
524
592
  const ws = new WebSocket(wsUrl);
525
593
  this.ws = ws;
526
594
  ws.onopen = () => {
527
- ws.send(JSON.stringify({ type: "login", token: session.accessToken }));
595
+ ws.send(JSON.stringify({ type: "login", token }));
528
596
  };
529
597
  ws.onmessage = (event) => {
530
598
  try {
@@ -825,6 +893,7 @@ var OperatorSessionClient = class {
825
893
  this.nextPlaybackTime = 0;
826
894
  }
827
895
  async disconnect() {
896
+ this.connectGeneration += 1;
828
897
  this.intentionalDisconnect = true;
829
898
  if (this.callPhase === "dialing" || this.callPhase === "live" || this.callPhase === "onHold") {
830
899
  this.hangUp();
@@ -853,6 +922,7 @@ export {
853
922
  ACTIVITY_CHANNEL,
854
923
  ACTIVITY_MEMBER_CHANNEL,
855
924
  AccountRealtimeClient,
925
+ LOGIN_TOKEN_TIMEOUT_MS,
856
926
  OperatorSessionClient,
857
927
  PRESENCE_CHANNEL,
858
928
  RealtimeSocketClient,