@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.
- package/README.md +44 -21
- package/dist/agui.d.cts +1 -1
- package/dist/agui.d.ts +1 -1
- package/dist/audio.d.cts +1 -1
- package/dist/audio.d.ts +1 -1
- package/dist/{chat-interaction-Ck1oMOr5.d.ts → chat-interaction-BB3ap8_u.d.ts} +8920 -6212
- package/dist/{chat-interaction-DqQu8Ad1.d.cts → chat-interaction-Bj1pciwo.d.cts} +8920 -6212
- package/dist/{chunk-P2E6LU7I.js → chunk-67QXTQVL.js} +1 -26
- package/dist/chunk-67QXTQVL.js.map +1 -0
- package/dist/chunk-6KPGCPGW.js +48 -0
- package/dist/chunk-6KPGCPGW.js.map +1 -0
- package/dist/{chunk-HRUMZ46V.js → chunk-72SWNLVD.js} +2 -1
- package/dist/chunk-72SWNLVD.js.map +1 -0
- package/dist/{chunk-R3C63RZJ.js → chunk-ZTUOB4V2.js} +1278 -218
- package/dist/chunk-ZTUOB4V2.js.map +1 -0
- package/dist/helpers.cjs +47 -0
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +4 -3
- package/dist/helpers.d.ts +4 -3
- package/dist/helpers.js +7 -1
- package/dist/helpers.js.map +1 -1
- package/dist/index.cjs +1389 -350
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -35
- package/dist/index.d.ts +13 -35
- package/dist/index.js +112 -122
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +1303 -252
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +338 -55
- package/dist/internal.d.ts +338 -55
- package/dist/internal.js +68 -21
- package/dist/internal.js.map +1 -1
- package/dist/member-import-D8fZr2Pd.d.cts +46 -0
- package/dist/member-import-YNk9MlEl.d.ts +46 -0
- package/dist/realtime.cjs +91 -20
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.d.cts +34 -9
- package/dist/realtime.d.ts +34 -9
- package/dist/realtime.js +90 -20
- package/dist/realtime.js.map +1 -1
- package/dist/{revision-state-Tn2V0-zt.d.cts → revision-state-BaJTq3Or.d.ts} +271 -55
- package/dist/{revision-state-V5RIhte0.d.ts → revision-state-CL11FA-T.d.cts} +271 -55
- package/dist/schemas.cjs +58 -81
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +88 -225
- package/dist/schemas.d.ts +88 -225
- package/dist/schemas.js +58 -76
- package/dist/schemas.js.map +1 -1
- package/dist/{tasks-BH_i_pVb.d.cts → tasks-ByJCQ5BI.d.cts} +3003 -2508
- package/dist/{tasks-BH_i_pVb.d.ts → tasks-ByJCQ5BI.d.ts} +3003 -2508
- package/dist/{types-0c6_NGU4.d.cts → types-DlfADjPR.d.cts} +17 -2
- package/dist/{types-0c6_NGU4.d.ts → types-DlfADjPR.d.ts} +17 -2
- package/package.json +9 -7
- package/dist/chunk-HRUMZ46V.js.map +0 -1
- package/dist/chunk-P2E6LU7I.js.map +0 -1
- package/dist/chunk-R3C63RZJ.js.map +0 -1
package/dist/realtime.d.cts
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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`:
|
|
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,
|
|
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.d.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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`:
|
|
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,
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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?.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|