@webex/internal-plugin-llm 3.12.0-next.4 → 3.12.0-next.40
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 +2 -2
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/llm.js +332 -47
- package/dist/llm.js.map +1 -1
- package/dist/llm.types.js +5 -1
- package/dist/llm.types.js.map +1 -1
- package/package.json +6 -6
- package/src/constants.ts +1 -0
- package/src/index.ts +3 -0
- package/src/llm.ts +344 -42
- package/src/llm.types.ts +55 -9
- package/test/unit/spec/llm.js +407 -19
package/src/llm.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
SUBSCRIPTION_AWARE_SUBCHANNELS_PARAM,
|
|
11
11
|
LLM_DEFAULT_SESSION,
|
|
12
12
|
} from './constants';
|
|
13
|
-
import {ILLMChannel, DataChannelTokenType} from './llm.types';
|
|
13
|
+
import {ILLMChannel, DataChannelTokenType, RegisterAndConnectTiming} from './llm.types';
|
|
14
14
|
|
|
15
15
|
export const config = {
|
|
16
16
|
llm: {
|
|
@@ -51,6 +51,7 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
|
|
|
51
51
|
defaultSessionId = LLM_DEFAULT_SESSION;
|
|
52
52
|
/**
|
|
53
53
|
* Map to store connection-specific data for multiple LLM connections
|
|
54
|
+
* Key: sessionId
|
|
54
55
|
* @private
|
|
55
56
|
* @type {Map<string, {webSocketUrl?: string; binding?: string; locusUrl?: string; datachannelUrl?: string}>}
|
|
56
57
|
*/
|
|
@@ -61,19 +62,20 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
|
|
|
61
62
|
binding?: string;
|
|
62
63
|
locusUrl?: string;
|
|
63
64
|
datachannelUrl?: string;
|
|
64
|
-
|
|
65
|
+
ownerMeetingId?: string;
|
|
66
|
+
refreshHandler?: () => Promise<{
|
|
67
|
+
body: {datachannelToken: string; datachannelTokenType: DataChannelTokenType};
|
|
68
|
+
}>;
|
|
65
69
|
}
|
|
66
70
|
> = new Map();
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
// Session-keyed token cache is intentionally decoupled from connection state.
|
|
73
|
+
// Disconnecting a socket session must not implicitly wipe token cache.
|
|
74
|
+
private datachannelTokens: Record<string, string | undefined> = {
|
|
69
75
|
[DataChannelTokenType.Default]: undefined,
|
|
70
76
|
[DataChannelTokenType.PracticeSession]: undefined,
|
|
71
77
|
};
|
|
72
78
|
|
|
73
|
-
private refreshHandler?: () => Promise<{
|
|
74
|
-
body: {datachannelToken: string; datachannelTokenType: DataChannelTokenType};
|
|
75
|
-
}>;
|
|
76
|
-
|
|
77
79
|
/**
|
|
78
80
|
* Register to the websocket
|
|
79
81
|
* @param {string} llmSocketUrl
|
|
@@ -123,16 +125,36 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
|
|
|
123
125
|
datachannelUrl: string,
|
|
124
126
|
datachannelToken?: string,
|
|
125
127
|
sessionId: string = LLM_DEFAULT_SESSION
|
|
126
|
-
): Promise<
|
|
127
|
-
|
|
128
|
-
if (!locusUrl || !datachannelUrl) return undefined;
|
|
128
|
+
): Promise<RegisterAndConnectTiming | undefined> => {
|
|
129
|
+
const registerStart = performance.now();
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
// Pre-populate locusUrl and datachannelUrl before register() fires the
|
|
132
|
+
// HTTP POST, so that any token refresh triggered during registration can
|
|
133
|
+
// be routed via connections without falling back to a locusInfo URL scan.
|
|
134
|
+
if (locusUrl && datachannelUrl) {
|
|
131
135
|
const sessionData = this.connections.get(sessionId) || {};
|
|
132
136
|
sessionData.locusUrl = locusUrl;
|
|
133
137
|
sessionData.datachannelUrl = datachannelUrl;
|
|
134
|
-
sessionData.datachannelToken = datachannelToken;
|
|
135
138
|
this.connections.set(sessionId, sessionData);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this.register(datachannelUrl, datachannelToken, sessionId).then(async () => {
|
|
142
|
+
if (!locusUrl || !datachannelUrl) return undefined;
|
|
143
|
+
|
|
144
|
+
const clientLLMDatachannelResponseTime = Math.round(performance.now() - registerStart);
|
|
145
|
+
|
|
146
|
+
// locusUrl and datachannelUrl were pre-populated before register(); here
|
|
147
|
+
// we only need to read the existing session data to get webSocketUrl/binding
|
|
148
|
+
// that register() filled in.
|
|
149
|
+
const sessionData = this.connections.get(sessionId) || {};
|
|
150
|
+
|
|
151
|
+
if (!sessionData.webSocketUrl) {
|
|
152
|
+
// register() succeeded but the response carried no websocket URL. Attach the measured
|
|
153
|
+
// datachannel time so callers don't misreport a registration that completed as time 0.
|
|
154
|
+
const error: any = new Error(`LLM registration for ${sessionId} returned no websocket URL`);
|
|
155
|
+
error.timing = {clientLLMDatachannelResponseTime};
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
136
158
|
|
|
137
159
|
const isDataChannelTokenEnabled = await this.isDataChannelTokenEnabled();
|
|
138
160
|
|
|
@@ -140,8 +162,23 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
|
|
|
140
162
|
? LLMChannel.buildUrlWithAwareSubchannels(sessionData.webSocketUrl, AWARE_DATA_CHANNEL)
|
|
141
163
|
: sessionData.webSocketUrl;
|
|
142
164
|
|
|
143
|
-
|
|
165
|
+
const connectStart = performance.now();
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await this.connect(connectUrl, sessionId);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// register() succeeded; only connect() failed. Attach the measured datachannel time so
|
|
171
|
+
// callers don't misreport a websocket failure as a registration that never completed.
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
error.timing = {clientLLMDatachannelResponseTime};
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const clientLLMWebSocketConnectTime = Math.round(performance.now() - connectStart);
|
|
178
|
+
|
|
179
|
+
return {clientLLMDatachannelResponseTime, clientLLMWebSocketConnectTime};
|
|
144
180
|
});
|
|
181
|
+
};
|
|
145
182
|
|
|
146
183
|
/**
|
|
147
184
|
* Tells if LLM socket is connected
|
|
@@ -187,73 +224,234 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
|
|
|
187
224
|
return sessionData?.datachannelUrl;
|
|
188
225
|
};
|
|
189
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Get WebSocket URL for the connection
|
|
229
|
+
* @param {string} sessionId - Connection identifier
|
|
230
|
+
* @returns {string | undefined} WebSocket URL
|
|
231
|
+
*/
|
|
232
|
+
public getWebSocketUrl = (sessionId = LLM_DEFAULT_SESSION): string | undefined => {
|
|
233
|
+
const sessionData = this.connections.get(sessionId);
|
|
234
|
+
|
|
235
|
+
return sessionData?.webSocketUrl;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Set the owner meeting ID for a given LLM session. Used by the meetings
|
|
240
|
+
* plugin to tag which Meeting instance currently owns the (default) LLM
|
|
241
|
+
* connection so that other Meeting instances can avoid disconnecting or
|
|
242
|
+
* re-initializing a connection they do not own.
|
|
243
|
+
*
|
|
244
|
+
* Does NOT create a connections entry if one does not already exist — this
|
|
245
|
+
* method is a no-op when there is no active session data. Callers should
|
|
246
|
+
* invoke it after a successful `registerAndConnect` or during an explicit
|
|
247
|
+
* ownership handoff.
|
|
248
|
+
*
|
|
249
|
+
* @param {string | undefined} ownerMeetingId - Meeting ID (or undefined to clear)
|
|
250
|
+
* @param {string} sessionId - Connection identifier (defaults to default session)
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
public setOwnerMeetingId = (
|
|
254
|
+
ownerMeetingId: string | undefined,
|
|
255
|
+
sessionId: string = LLM_DEFAULT_SESSION
|
|
256
|
+
): void => {
|
|
257
|
+
const sessionData = this.connections.get(sessionId);
|
|
258
|
+
|
|
259
|
+
if (!sessionData) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
sessionData.ownerMeetingId = ownerMeetingId;
|
|
264
|
+
this.connections.set(sessionId, sessionData);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the owner meeting ID currently associated with an LLM session.
|
|
269
|
+
* Returns undefined when no owner has been assigned (e.g. before the
|
|
270
|
+
* first successful `registerAndConnect`, or after `disconnectLLM`).
|
|
271
|
+
*
|
|
272
|
+
* @param {string} sessionId - Connection identifier (defaults to default session)
|
|
273
|
+
* @returns {string | undefined} ownerMeetingId
|
|
274
|
+
*/
|
|
275
|
+
public getOwnerMeetingId = (sessionId: string = LLM_DEFAULT_SESSION): string | undefined => {
|
|
276
|
+
const sessionData = this.connections.get(sessionId);
|
|
277
|
+
|
|
278
|
+
return sessionData?.ownerMeetingId;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Resolve ownership information for an LLM session.
|
|
283
|
+
*
|
|
284
|
+
* Rules:
|
|
285
|
+
* - no current owner => caller may proceed
|
|
286
|
+
* - caller has no identity to assert => treat as owner
|
|
287
|
+
* - otherwise caller must match current owner
|
|
288
|
+
*
|
|
289
|
+
* @param {string | undefined} ownerMeetingId - Candidate owner to evaluate
|
|
290
|
+
* @param {string} sessionId - Connection identifier (defaults to default session)
|
|
291
|
+
* @returns {{currentOwner: (string|undefined), isOwner: boolean}}
|
|
292
|
+
*/
|
|
293
|
+
public resolveSessionOwnership = (
|
|
294
|
+
ownerMeetingId?: string,
|
|
295
|
+
sessionId: string = LLM_DEFAULT_SESSION
|
|
296
|
+
): {
|
|
297
|
+
currentOwner: string | undefined;
|
|
298
|
+
isOwner: boolean;
|
|
299
|
+
} => {
|
|
300
|
+
const currentOwner = this.getOwnerMeetingId(sessionId);
|
|
301
|
+
const isOwner = !currentOwner || !ownerMeetingId || currentOwner === ownerMeetingId;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
currentOwner,
|
|
305
|
+
isOwner,
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
190
309
|
/**
|
|
191
310
|
* Get data channel token for the connection
|
|
192
|
-
* @param {DataChannelTokenType}
|
|
193
|
-
* @
|
|
311
|
+
* @param {DataChannelTokenType|string} tokenKey
|
|
312
|
+
* @param {string | undefined} ownerMeetingId - Meeting id asserting read ownership
|
|
313
|
+
* @returns {string | undefined} data channel token
|
|
194
314
|
*/
|
|
195
315
|
public getDatachannelToken = (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
316
|
+
tokenKey?: DataChannelTokenType | string,
|
|
317
|
+
ownerMeetingId?: string
|
|
318
|
+
): string | undefined => {
|
|
319
|
+
const resolvedTokenKey = tokenKey ?? DataChannelTokenType.Default;
|
|
320
|
+
|
|
321
|
+
const {currentOwner, isOwner} = this.resolveSessionOwnership(ownerMeetingId, resolvedTokenKey);
|
|
322
|
+
|
|
323
|
+
if (!isOwner) {
|
|
324
|
+
this.logger.info(
|
|
325
|
+
`llm#getDatachannelToken --> skip read for session ${resolvedTokenKey}; owned by ${currentOwner}, candidate ${ownerMeetingId}`
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return this.datachannelTokens[resolvedTokenKey];
|
|
199
332
|
};
|
|
200
333
|
|
|
201
334
|
/**
|
|
202
335
|
* Set data channel token for the connection
|
|
203
336
|
* @param {string} datachannelToken - data channel token
|
|
204
|
-
* @param {DataChannelTokenType}
|
|
337
|
+
* @param {DataChannelTokenType|string} [tokenKey]
|
|
338
|
+
* @param {string | undefined} ownerMeetingId - Meeting id asserting write ownership
|
|
205
339
|
* @returns {void}
|
|
206
340
|
*/
|
|
207
341
|
public setDatachannelToken = (
|
|
208
342
|
datachannelToken: string,
|
|
209
|
-
|
|
343
|
+
tokenKey?: DataChannelTokenType | string,
|
|
344
|
+
ownerMeetingId?: string
|
|
210
345
|
): void => {
|
|
211
|
-
|
|
346
|
+
const resolvedTokenKey = tokenKey ?? DataChannelTokenType.Default;
|
|
347
|
+
|
|
348
|
+
const {currentOwner, isOwner} = this.resolveSessionOwnership(ownerMeetingId, resolvedTokenKey);
|
|
349
|
+
|
|
350
|
+
if (!isOwner) {
|
|
351
|
+
this.logger.info(
|
|
352
|
+
`llm#setDatachannelToken --> skip write for session ${resolvedTokenKey}; owned by ${currentOwner}, candidate ${ownerMeetingId}`
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.datachannelTokens[resolvedTokenKey] = datachannelToken;
|
|
212
359
|
};
|
|
213
360
|
|
|
214
361
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
362
|
+
* Clears a single session's data channel token.
|
|
363
|
+
* @param {DataChannelTokenType|string} tokenKey
|
|
364
|
+
* @param {string} ownerMeetingId - Meeting id asserting delete ownership
|
|
217
365
|
* @returns {void}
|
|
218
366
|
*/
|
|
219
|
-
public
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
367
|
+
public clearDatachannelToken = (
|
|
368
|
+
tokenKey: DataChannelTokenType | string,
|
|
369
|
+
ownerMeetingId: string
|
|
370
|
+
): void => {
|
|
371
|
+
const resolvedTokenKey = tokenKey;
|
|
372
|
+
|
|
373
|
+
const {currentOwner, isOwner} = this.resolveSessionOwnership(ownerMeetingId, resolvedTokenKey);
|
|
374
|
+
|
|
375
|
+
if (!isOwner) {
|
|
376
|
+
this.logger.info(
|
|
377
|
+
`llm#clearDatachannelToken --> skip clear for session ${resolvedTokenKey}; owned by ${currentOwner}, candidate ${ownerMeetingId}`
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.datachannelTokens[resolvedTokenKey] = undefined;
|
|
384
|
+
delete this.datachannelTokens[resolvedTokenKey];
|
|
385
|
+
};
|
|
225
386
|
|
|
226
387
|
/**
|
|
227
388
|
* Set the handler used to refresh the DataChannel token
|
|
228
389
|
*
|
|
229
390
|
* @param {function} handler - Function that returns a refreshed token
|
|
391
|
+
* @param {string} [sessionId] - Connection identifier
|
|
392
|
+
* @param {string | undefined} ownerMeetingId - Meeting id asserting refresh-handler ownership
|
|
230
393
|
* @returns {void}
|
|
231
394
|
*/
|
|
232
395
|
public setRefreshHandler(
|
|
233
396
|
handler: () => Promise<{
|
|
234
397
|
body: {datachannelToken: string; datachannelTokenType: DataChannelTokenType};
|
|
235
|
-
}
|
|
398
|
+
}>,
|
|
399
|
+
sessionId?: string,
|
|
400
|
+
ownerMeetingId?: string
|
|
236
401
|
) {
|
|
237
|
-
|
|
402
|
+
const resolvedSessionId = sessionId ?? LLM_DEFAULT_SESSION;
|
|
403
|
+
|
|
404
|
+
const {currentOwner, isOwner} = this.resolveSessionOwnership(ownerMeetingId, resolvedSessionId);
|
|
405
|
+
|
|
406
|
+
if (!isOwner) {
|
|
407
|
+
this.logger.info(
|
|
408
|
+
`llm#setRefreshHandler --> skip write for session ${resolvedSessionId}; owned by ${currentOwner}, candidate ${ownerMeetingId}`
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const sessionData = this.connections.get(resolvedSessionId);
|
|
415
|
+
|
|
416
|
+
if (sessionData) {
|
|
417
|
+
sessionData.refreshHandler = handler;
|
|
418
|
+
if (ownerMeetingId) {
|
|
419
|
+
sessionData.ownerMeetingId = ownerMeetingId;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Intentionally allow a pre-connection session shape here.
|
|
426
|
+
// Some flows inject refreshHandler before register/connect so token refresh
|
|
427
|
+
// is already wired when the socket lifecycle starts. register()/
|
|
428
|
+
// registerAndConnect() will later fill webSocketUrl/binding/locusUrl/
|
|
429
|
+
// datachannelUrl into this same session entry.
|
|
430
|
+
this.connections.set(resolvedSessionId, {
|
|
431
|
+
refreshHandler: handler,
|
|
432
|
+
ownerMeetingId,
|
|
433
|
+
});
|
|
238
434
|
}
|
|
239
435
|
|
|
240
436
|
/**
|
|
241
437
|
* Refresh the data channel token using the injected handler.
|
|
242
438
|
* Logs a descriptive error if the handler is missing or fails.
|
|
243
|
-
*
|
|
439
|
+
* @param {string} sessionId - Connection identifier (defaults to default session)
|
|
244
440
|
* @returns {Promise<string>} The refreshed token.
|
|
245
441
|
*/
|
|
246
|
-
public async refreshDataChannelToken() {
|
|
247
|
-
|
|
442
|
+
public async refreshDataChannelToken(sessionId: string = LLM_DEFAULT_SESSION) {
|
|
443
|
+
const refreshHandler = this.connections.get(sessionId)?.refreshHandler;
|
|
444
|
+
|
|
445
|
+
if (!refreshHandler) {
|
|
248
446
|
this.logger.warn(
|
|
249
|
-
|
|
447
|
+
`llm#refreshDataChannelToken --> LLM refreshHandler is not set for session ${sessionId}, skipping token refresh`
|
|
250
448
|
);
|
|
251
449
|
|
|
252
450
|
return null;
|
|
253
451
|
}
|
|
254
452
|
|
|
255
453
|
try {
|
|
256
|
-
const res = await
|
|
454
|
+
const res = await refreshHandler();
|
|
257
455
|
|
|
258
456
|
return res;
|
|
259
457
|
} catch (error: any) {
|
|
@@ -271,16 +469,51 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
|
|
|
271
469
|
* Disconnects websocket connection
|
|
272
470
|
* @param {{code: number, reason: string}} options - The disconnect option object with code and reason
|
|
273
471
|
* @param {string} sessionId - Connection identifier
|
|
274
|
-
* @
|
|
472
|
+
* @param {string} ownerMeetingId - Meeting id asserting disconnect ownership
|
|
473
|
+
* @returns {Promise<boolean>} True when disconnect was performed, false when skipped
|
|
275
474
|
*/
|
|
276
475
|
public disconnectLLM = (
|
|
277
476
|
options: {code: number; reason: string},
|
|
278
|
-
sessionId
|
|
279
|
-
|
|
280
|
-
|
|
477
|
+
sessionId?: string,
|
|
478
|
+
ownerMeetingId?: string
|
|
479
|
+
): Promise<boolean> => {
|
|
480
|
+
const resolvedSessionId = sessionId ?? LLM_DEFAULT_SESSION;
|
|
481
|
+
|
|
482
|
+
// Backward-compat path: historically callers could omit ownerMeetingId
|
|
483
|
+
// (and sometimes sessionId). Reuse current owner when available so legacy
|
|
484
|
+
// calls remain best-effort without throwing at teardown time.
|
|
485
|
+
const resolvedOwnerMeetingId = ownerMeetingId || this.getOwnerMeetingId(resolvedSessionId);
|
|
486
|
+
|
|
487
|
+
if (!ownerMeetingId) {
|
|
488
|
+
this.logger.warn(
|
|
489
|
+
`llm#disconnectLLM --> ownerMeetingId is omitted for session ${resolvedSessionId}; using legacy compatibility path`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const {currentOwner, isOwner} = this.resolveSessionOwnership(
|
|
494
|
+
resolvedOwnerMeetingId,
|
|
495
|
+
resolvedSessionId
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
if (!isOwner) {
|
|
499
|
+
this.logger.info(
|
|
500
|
+
`llm#disconnectLLM --> skip disconnect for session ${resolvedSessionId}; owned by ${currentOwner}, candidate ${resolvedOwnerMeetingId}`
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
return Promise.resolve(false);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return this.disconnect(options, resolvedSessionId).then(() => {
|
|
507
|
+
// Clear owner tag before cleanup to ensure it's not lingering
|
|
508
|
+
// if another meeting claimed it during disconnect
|
|
509
|
+
this.setOwnerMeetingId(undefined, resolvedSessionId);
|
|
510
|
+
|
|
281
511
|
// Clean up sessions data
|
|
282
|
-
this.connections.delete(
|
|
512
|
+
this.connections.delete(resolvedSessionId);
|
|
513
|
+
|
|
514
|
+
return true;
|
|
283
515
|
});
|
|
516
|
+
};
|
|
284
517
|
|
|
285
518
|
/**
|
|
286
519
|
* Disconnects all LLM websocket connections
|
|
@@ -304,10 +537,79 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
|
|
|
304
537
|
binding?: string;
|
|
305
538
|
locusUrl?: string;
|
|
306
539
|
datachannelUrl?: string;
|
|
307
|
-
|
|
540
|
+
ownerMeetingId?: string;
|
|
308
541
|
}
|
|
309
542
|
> => new Map(this.connections);
|
|
310
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Look up the locusUrl associated with a datachannel request URL.
|
|
546
|
+
* Iterates all active LLM sessions and returns the locusUrl of the
|
|
547
|
+
* session whose stored datachannelUrl is a prefix of the given request URL.
|
|
548
|
+
*
|
|
549
|
+
* @param {string} requestUrl - The in-flight request URL to match
|
|
550
|
+
* @returns {string | undefined} The matching locusUrl, or undefined if not found
|
|
551
|
+
*/
|
|
552
|
+
public getLocusUrlByDatachannelUrl(requestUrl: string): string | undefined {
|
|
553
|
+
for (const [, connection] of this.connections) {
|
|
554
|
+
if (
|
|
555
|
+
connection.datachannelUrl &&
|
|
556
|
+
LLMChannel.matchesDatachannelRequestUrl(requestUrl, connection.datachannelUrl)
|
|
557
|
+
) {
|
|
558
|
+
return connection.locusUrl;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Look up the sessionId associated with a datachannel request URL.
|
|
567
|
+
* Iterates all active LLM sessions and returns the sessionId whose
|
|
568
|
+
* stored datachannelUrl is a prefix of the given request URL.
|
|
569
|
+
*
|
|
570
|
+
* @param {string} requestUrl - The in-flight request URL to match
|
|
571
|
+
* @returns {string | undefined} The matching sessionId, or undefined if not found
|
|
572
|
+
*/
|
|
573
|
+
public getSessionIdByDatachannelUrl(requestUrl: string): string | undefined {
|
|
574
|
+
for (const [sessionId, connection] of this.connections) {
|
|
575
|
+
if (
|
|
576
|
+
connection.datachannelUrl &&
|
|
577
|
+
LLMChannel.matchesDatachannelRequestUrl(requestUrl, connection.datachannelUrl)
|
|
578
|
+
) {
|
|
579
|
+
return sessionId;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Matches a request URL to a stored datachannel registration URL.
|
|
588
|
+
* Host can differ (e.g. rewritten by hostmap interceptor), so we first
|
|
589
|
+
* try full URL prefix and then fall back to pathname prefix.
|
|
590
|
+
* @param {string} requestUrl
|
|
591
|
+
* @param {string} registrationUrl
|
|
592
|
+
* @returns {boolean}
|
|
593
|
+
*/
|
|
594
|
+
public static matchesDatachannelRequestUrl(requestUrl: string, registrationUrl: string): boolean {
|
|
595
|
+
if (!requestUrl || !registrationUrl) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (requestUrl.startsWith(registrationUrl)) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const request = new URL(requestUrl);
|
|
605
|
+
const registration = new URL(registrationUrl);
|
|
606
|
+
|
|
607
|
+
return request.pathname.startsWith(registration.pathname);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
311
613
|
/**
|
|
312
614
|
* Returns true if data channel token is enabled, false otherwise
|
|
313
615
|
* @returns {Promise<boolean>} resolves with true if data channel token is enabled
|
package/src/llm.types.ts
CHANGED
|
@@ -1,16 +1,67 @@
|
|
|
1
|
+
export enum DataChannelTokenType {
|
|
2
|
+
Default = 'llm-default-session',
|
|
3
|
+
PracticeSession = 'llm-practice-session',
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type DataChannelTokenKey = DataChannelTokenType | string;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Latencies (in milliseconds) captured during register + websocket connect.
|
|
10
|
+
*/
|
|
11
|
+
type RegisterAndConnectTiming = {
|
|
12
|
+
clientLLMDatachannelResponseTime?: number;
|
|
13
|
+
clientLLMWebSocketConnectTime?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
1
16
|
interface ILLMChannel {
|
|
2
17
|
registerAndConnect: (
|
|
3
18
|
locusUrl: string,
|
|
4
19
|
datachannelUrl: string,
|
|
5
20
|
datachannelToken?: string,
|
|
6
21
|
sessionId?: string
|
|
7
|
-
) => Promise<
|
|
22
|
+
) => Promise<RegisterAndConnectTiming | undefined>;
|
|
8
23
|
isConnected: (sessionId?: string) => boolean;
|
|
9
24
|
getBinding: (sessionId?: string) => string;
|
|
10
25
|
getLocusUrl: (sessionId?: string) => string;
|
|
11
26
|
getDatachannelUrl: (sessionId?: string) => string;
|
|
12
|
-
|
|
27
|
+
getWebSocketUrl: (sessionId?: string) => string | undefined;
|
|
28
|
+
disconnectLLM: (
|
|
29
|
+
options: {code: number; reason: string},
|
|
30
|
+
sessionId?: string,
|
|
31
|
+
ownerMeetingId?: string
|
|
32
|
+
) => Promise<boolean>;
|
|
13
33
|
disconnectAllLLM: (options?: {code: number; reason: string}) => Promise<void>;
|
|
34
|
+
setOwnerMeetingId: (ownerMeetingId: string | undefined, sessionId?: string) => void;
|
|
35
|
+
getOwnerMeetingId: (sessionId?: string) => string | undefined;
|
|
36
|
+
resolveSessionOwnership: (
|
|
37
|
+
ownerMeetingId?: string,
|
|
38
|
+
sessionId?: string
|
|
39
|
+
) => {
|
|
40
|
+
currentOwner: string | undefined;
|
|
41
|
+
isOwner: boolean;
|
|
42
|
+
};
|
|
43
|
+
getDatachannelToken: (
|
|
44
|
+
tokenKey?: DataChannelTokenKey,
|
|
45
|
+
ownerMeetingId?: string
|
|
46
|
+
) => string | undefined;
|
|
47
|
+
setDatachannelToken: (
|
|
48
|
+
datachannelToken: string,
|
|
49
|
+
tokenKey?: DataChannelTokenKey,
|
|
50
|
+
ownerMeetingId?: string
|
|
51
|
+
) => void;
|
|
52
|
+
clearDatachannelToken: (tokenKey: DataChannelTokenKey, ownerMeetingId: string) => void;
|
|
53
|
+
setRefreshHandler: (
|
|
54
|
+
handler: () => Promise<{
|
|
55
|
+
body: {datachannelToken: string; datachannelTokenType: DataChannelTokenType};
|
|
56
|
+
}>,
|
|
57
|
+
sessionId?: string,
|
|
58
|
+
ownerMeetingId?: string
|
|
59
|
+
) => void;
|
|
60
|
+
refreshDataChannelToken: (sessionId?: string) => Promise<{
|
|
61
|
+
body: {datachannelToken: string; datachannelTokenType: DataChannelTokenType};
|
|
62
|
+
} | null>;
|
|
63
|
+
getLocusUrlByDatachannelUrl: (requestUrl: string) => string | undefined;
|
|
64
|
+
getSessionIdByDatachannelUrl: (requestUrl: string) => string | undefined;
|
|
14
65
|
getAllConnections: () => Map<
|
|
15
66
|
string,
|
|
16
67
|
{
|
|
@@ -18,15 +69,10 @@ interface ILLMChannel {
|
|
|
18
69
|
binding?: string;
|
|
19
70
|
locusUrl?: string;
|
|
20
71
|
datachannelUrl?: string;
|
|
21
|
-
|
|
72
|
+
ownerMeetingId?: string;
|
|
22
73
|
}
|
|
23
74
|
>;
|
|
24
75
|
}
|
|
25
76
|
|
|
26
|
-
export enum DataChannelTokenType {
|
|
27
|
-
Default = 'llm-default-session',
|
|
28
|
-
PracticeSession = 'llm-practice-session',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
77
|
// eslint-disable-next-line import/prefer-default-export
|
|
32
|
-
export type {ILLMChannel};
|
|
78
|
+
export type {ILLMChannel, DataChannelTokenKey, RegisterAndConnectTiming};
|