@nuancelabs/js-sdk 0.2.2 → 0.4.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/index.d.ts +2 -8
- package/index.js +163 -18
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -216,8 +216,8 @@ export declare type ConversationEvent =
|
|
|
216
216
|
/**
|
|
217
217
|
* How long the live session has left, when it is capped.
|
|
218
218
|
*
|
|
219
|
-
* Emitted
|
|
220
|
-
* countdown should
|
|
219
|
+
* Emitted when the time left is learned or corrected, so it steps rather
|
|
220
|
+
* than ticks; a caller showing a countdown should count down from the latest.
|
|
221
221
|
*/
|
|
222
222
|
| {
|
|
223
223
|
type: "time-remaining";
|
|
@@ -344,9 +344,6 @@ export declare interface CreateNuanceConversationOptions {
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
347
|
/**
|
|
351
348
|
* Raised by {@link Conversation.connect} when the server refuses a new session
|
|
352
349
|
* because the account has not completed onboarding or accepted the current
|
|
@@ -406,7 +403,6 @@ export declare type MediaErrorReason = "permission-denied" | "device-not-found"
|
|
|
406
403
|
export declare type NetworkQuality = "good" | "poor" | "lost";
|
|
407
404
|
|
|
408
405
|
|
|
409
|
-
|
|
410
406
|
/**
|
|
411
407
|
* Raised by {@link Conversation.connect} when the server refuses a new session
|
|
412
408
|
* because the runtime recently ended too many of this account's sessions for
|
|
@@ -428,11 +424,9 @@ export declare type SessionEndReason = "time-limit" | "ended" | "policy-violatio
|
|
|
428
424
|
|
|
429
425
|
|
|
430
426
|
|
|
431
|
-
|
|
432
427
|
/** When the avatar starts speaking: see {@link CreateNuanceConversationOptions.startOn}. */
|
|
433
428
|
export declare type StartOn = "first-frame" | "full-quality";
|
|
434
429
|
|
|
435
|
-
|
|
436
430
|
/**
|
|
437
431
|
* The local user participant in a conversation.
|
|
438
432
|
*/
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ function isRecord(value) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
// src/core/sdk-version.ts
|
|
7
|
-
var SDK_HEADER = `js/${"0.
|
|
7
|
+
var SDK_HEADER = `js/${"0.4.0"}`;
|
|
8
8
|
|
|
9
9
|
// src/utils.ts
|
|
10
10
|
function normalizeHost(host) {
|
|
@@ -56,6 +56,9 @@ class NuanceApiClient {
|
|
|
56
56
|
async heartbeatConversation(conversationId) {
|
|
57
57
|
return await this.request(`/conversation/${conversationId}/heartbeat`, { method: "POST", token: this.tokens.get(conversationId) });
|
|
58
58
|
}
|
|
59
|
+
async conversationEvents(conversationId, after, signal) {
|
|
60
|
+
return await this.request(`/conversation/${conversationId}/events?after=${after}`, { token: this.tokens.get(conversationId), signal });
|
|
61
|
+
}
|
|
59
62
|
async abandonConversation(conversationId) {
|
|
60
63
|
return await this.request(`/conversation/${conversationId}/abandon`, { method: "POST", token: this.tokens.get(conversationId) });
|
|
61
64
|
}
|
|
@@ -71,7 +74,8 @@ class NuanceApiClient {
|
|
|
71
74
|
const response = await fetch(`${this.host}${path}`, {
|
|
72
75
|
method: options?.method ?? "GET",
|
|
73
76
|
headers,
|
|
74
|
-
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined
|
|
77
|
+
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
78
|
+
signal: options?.signal
|
|
75
79
|
});
|
|
76
80
|
if (!response.ok) {
|
|
77
81
|
let detail = response.statusText;
|
|
@@ -2137,7 +2141,8 @@ class LiveKitConversation {
|
|
|
2137
2141
|
var DEFAULT_HOST = "https://api.nuancelabs.ai";
|
|
2138
2142
|
var QUEUE_HEARTBEAT_INTERVAL_MS = 5000;
|
|
2139
2143
|
var HEARTBEAT_INTERVAL_MS = 5000;
|
|
2140
|
-
var
|
|
2144
|
+
var EVENTS_RETRY_MS = 5000;
|
|
2145
|
+
var ADMISSION_FAILED_REQUESTS = 6;
|
|
2141
2146
|
var BUSY_RETRY_LIMIT_MS = 60000;
|
|
2142
2147
|
var BUSY_RETRY_DEFAULT_MS = 3000;
|
|
2143
2148
|
var LOST_WINDOW_MS = 30000;
|
|
@@ -2204,6 +2209,7 @@ class NuanceConversation {
|
|
|
2204
2209
|
delegate = null;
|
|
2205
2210
|
delegateUnsubscribe = null;
|
|
2206
2211
|
stopBeats = null;
|
|
2212
|
+
stopEvents = null;
|
|
2207
2213
|
admitted = null;
|
|
2208
2214
|
failedBeats = 0;
|
|
2209
2215
|
resilient = false;
|
|
@@ -2292,10 +2298,8 @@ class NuanceConversation {
|
|
|
2292
2298
|
this._id = conversation.id;
|
|
2293
2299
|
this.held = conversation.id;
|
|
2294
2300
|
this.emit({ type: "session-created", id: conversation.id });
|
|
2295
|
-
|
|
2296
|
-
this.
|
|
2297
|
-
await this.admitted.promise;
|
|
2298
|
-
this.delegate = await this.createDelegate(conversation, mediaDevices, attempt);
|
|
2301
|
+
const grant = await this.waitForSlot(conversation.id, attempt);
|
|
2302
|
+
this.delegate = await this.createDelegate(conversation, grant, mediaDevices, attempt);
|
|
2299
2303
|
this.delegateUnsubscribe = this.delegate.onEvent((event) => {
|
|
2300
2304
|
if (event.type === "state-changed") {
|
|
2301
2305
|
this.setState(event.state);
|
|
@@ -2318,7 +2322,9 @@ class NuanceConversation {
|
|
|
2318
2322
|
this.emit(event);
|
|
2319
2323
|
});
|
|
2320
2324
|
await this.delegate.connect();
|
|
2321
|
-
|
|
2325
|
+
if (grant === null) {
|
|
2326
|
+
this.startHeartbeat(conversation.id, HEARTBEAT_INTERVAL_MS);
|
|
2327
|
+
}
|
|
2322
2328
|
} catch (error) {
|
|
2323
2329
|
if (attempt !== this.attempt) {
|
|
2324
2330
|
throw error;
|
|
@@ -2332,6 +2338,8 @@ class NuanceConversation {
|
|
|
2332
2338
|
this.attempt += 1;
|
|
2333
2339
|
this.clearLimit();
|
|
2334
2340
|
this.stopHeartbeat();
|
|
2341
|
+
this.stopEvents?.();
|
|
2342
|
+
this.stopEvents = null;
|
|
2335
2343
|
this.admitted?.reject(new Error("Disconnected while waiting for a slot"));
|
|
2336
2344
|
this.admitted = null;
|
|
2337
2345
|
this.delegateUnsubscribe?.();
|
|
@@ -2390,19 +2398,146 @@ class NuanceConversation {
|
|
|
2390
2398
|
}
|
|
2391
2399
|
});
|
|
2392
2400
|
}
|
|
2393
|
-
async
|
|
2401
|
+
async waitForSlot(conversationId, attempt) {
|
|
2402
|
+
const admission = await this.followEvents(conversationId, attempt);
|
|
2403
|
+
if (admission !== null) {
|
|
2404
|
+
this.followLive(conversationId, admission.after, attempt);
|
|
2405
|
+
return admission.grant;
|
|
2406
|
+
}
|
|
2407
|
+
this.admitted = Promise.withResolvers();
|
|
2408
|
+
this.startHeartbeat(conversationId, QUEUE_HEARTBEAT_INTERVAL_MS);
|
|
2409
|
+
await this.admitted.promise;
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
async followEvents(conversationId, attempt) {
|
|
2413
|
+
const abort = new AbortController;
|
|
2414
|
+
const stop = () => abort.abort();
|
|
2415
|
+
this.stopEvents = stop;
|
|
2416
|
+
let after = 0;
|
|
2417
|
+
let answered = false;
|
|
2418
|
+
let failures = 0;
|
|
2419
|
+
try {
|
|
2420
|
+
for (;; ) {
|
|
2421
|
+
let events;
|
|
2422
|
+
try {
|
|
2423
|
+
({ events } = await this.api.conversationEvents(conversationId, after, abort.signal));
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
if (attempt !== this.attempt)
|
|
2426
|
+
throw new DisconnectedError;
|
|
2427
|
+
if (!answered && error instanceof ApiError && error.status === 404) {
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
failures += 1;
|
|
2431
|
+
if (failures >= ADMISSION_FAILED_REQUESTS) {
|
|
2432
|
+
throw new Error("Lost contact with the server while waiting", {
|
|
2433
|
+
cause: error
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
await this.pause(EVENTS_RETRY_MS, abort.signal);
|
|
2437
|
+
if (attempt !== this.attempt)
|
|
2438
|
+
throw new DisconnectedError;
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
if (attempt !== this.attempt)
|
|
2442
|
+
throw new DisconnectedError;
|
|
2443
|
+
answered = true;
|
|
2444
|
+
failures = 0;
|
|
2445
|
+
for (const event of events) {
|
|
2446
|
+
if (event.type === "admitted") {
|
|
2447
|
+
const grant = {
|
|
2448
|
+
server_url: event.livekit_url,
|
|
2449
|
+
participant_token: event.token
|
|
2450
|
+
};
|
|
2451
|
+
return { grant, after: event.id };
|
|
2452
|
+
}
|
|
2453
|
+
after = Math.max(after, event.id);
|
|
2454
|
+
if (event.type === "ended") {
|
|
2455
|
+
throw new TerminalStatusError(event.reason);
|
|
2456
|
+
}
|
|
2457
|
+
if (event.type === "queued" && event.position != null) {
|
|
2458
|
+
this.reportPlace(event.position, event.estimated_wait_seconds ?? null, null);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
} finally {
|
|
2463
|
+
if (this.stopEvents === stop)
|
|
2464
|
+
this.stopEvents = null;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
async followLive(conversationId, after, attempt) {
|
|
2468
|
+
const abort = new AbortController;
|
|
2469
|
+
const stop = () => abort.abort();
|
|
2470
|
+
this.stopEvents = stop;
|
|
2471
|
+
let failures = 0;
|
|
2472
|
+
try {
|
|
2473
|
+
while (attempt === this.attempt && !this.ended) {
|
|
2474
|
+
let events;
|
|
2475
|
+
try {
|
|
2476
|
+
({ events } = await this.api.conversationEvents(conversationId, after, abort.signal));
|
|
2477
|
+
} catch {
|
|
2478
|
+
if (attempt !== this.attempt)
|
|
2479
|
+
return;
|
|
2480
|
+
failures += 1;
|
|
2481
|
+
await this.pause(EVENTS_RETRY_MS, abort.signal);
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
if (attempt !== this.attempt)
|
|
2485
|
+
return;
|
|
2486
|
+
const receivedAt = Date.now();
|
|
2487
|
+
this.lostContact = failures > 0;
|
|
2488
|
+
failures = 0;
|
|
2489
|
+
for (const event of events) {
|
|
2490
|
+
after = Math.max(after, event.id);
|
|
2491
|
+
if (event.type === "started" && event.seconds_remaining != null) {
|
|
2492
|
+
this.capped = true;
|
|
2493
|
+
this.emit({
|
|
2494
|
+
type: "time-remaining",
|
|
2495
|
+
secondsRemaining: event.seconds_remaining
|
|
2496
|
+
});
|
|
2497
|
+
this.armLimit(event.seconds_remaining, receivedAt);
|
|
2498
|
+
}
|
|
2499
|
+
if (event.type === "ended") {
|
|
2500
|
+
this.endSession(this.endReason(event.reason));
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
} finally {
|
|
2506
|
+
if (this.stopEvents === stop)
|
|
2507
|
+
this.stopEvents = null;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
endReason(reason) {
|
|
2511
|
+
if (reason === "time_limit" || this.atLimit())
|
|
2512
|
+
return "time-limit";
|
|
2513
|
+
if (reason === "policy_violation")
|
|
2514
|
+
return "policy-violation";
|
|
2515
|
+
if (this.cutOff())
|
|
2516
|
+
return "connection-lost";
|
|
2517
|
+
return "ended";
|
|
2518
|
+
}
|
|
2519
|
+
pause(ms, signal) {
|
|
2520
|
+
return new Promise((resolve) => {
|
|
2521
|
+
let stopTicker = () => {};
|
|
2522
|
+
const done = () => {
|
|
2523
|
+
stopTicker();
|
|
2524
|
+
signal.removeEventListener("abort", done);
|
|
2525
|
+
resolve();
|
|
2526
|
+
};
|
|
2527
|
+
stopTicker = this.ticker(done, ms);
|
|
2528
|
+
signal.addEventListener("abort", done);
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
async createDelegate(conversation, grant, mediaDevices, attempt) {
|
|
2394
2532
|
if (conversation.provider !== "livekit") {
|
|
2395
2533
|
throw new Error(`Unsupported provider ${conversation.provider}`);
|
|
2396
2534
|
}
|
|
2397
|
-
const
|
|
2535
|
+
const connection = grant ?? await this.connectRoom(conversation.id);
|
|
2398
2536
|
if (attempt !== this.attempt) {
|
|
2399
2537
|
throw new DisconnectedError;
|
|
2400
2538
|
}
|
|
2401
|
-
if (response.connection.kind !== "livekit") {
|
|
2402
|
-
throw new Error(`Expected LiveKit connection, received ${response.connection.kind}`);
|
|
2403
|
-
}
|
|
2404
2539
|
this.resilient = typeof this.resilientReconnect === "function" ? this.resilientReconnect() : this.resilientReconnect;
|
|
2405
|
-
return this.provider(
|
|
2540
|
+
return this.provider(connection, {
|
|
2406
2541
|
conversationId: conversation.id,
|
|
2407
2542
|
mediaDevices,
|
|
2408
2543
|
cameraEnabled: !this.sessionOverrides.disable_camera && mediaDevices.cameraEnabled !== false,
|
|
@@ -2411,6 +2546,13 @@ class NuanceConversation {
|
|
|
2411
2546
|
resilientReconnect: this.resilient
|
|
2412
2547
|
});
|
|
2413
2548
|
}
|
|
2549
|
+
async connectRoom(conversationId) {
|
|
2550
|
+
const response = await this.api.connectConversation(conversationId);
|
|
2551
|
+
if (response.connection.kind !== "livekit") {
|
|
2552
|
+
throw new Error(`Expected LiveKit connection, received ${response.connection.kind}`);
|
|
2553
|
+
}
|
|
2554
|
+
return response.connection;
|
|
2555
|
+
}
|
|
2414
2556
|
async createWhenNotBusy(request, attempt) {
|
|
2415
2557
|
const giveUpAt = Date.now() + BUSY_RETRY_LIMIT_MS;
|
|
2416
2558
|
for (;; ) {
|
|
@@ -2458,7 +2600,7 @@ class NuanceConversation {
|
|
|
2458
2600
|
return;
|
|
2459
2601
|
if (!(error instanceof TerminalStatusError)) {
|
|
2460
2602
|
this.failedBeats += 1;
|
|
2461
|
-
if (this.admitted && this.failedBeats >=
|
|
2603
|
+
if (this.admitted && this.failedBeats >= ADMISSION_FAILED_REQUESTS) {
|
|
2462
2604
|
this.admitted.reject(new Error("Lost contact with the server while waiting", {
|
|
2463
2605
|
cause: error
|
|
2464
2606
|
}));
|
|
@@ -2497,14 +2639,17 @@ class NuanceConversation {
|
|
|
2497
2639
|
if (state.status !== "queued" || state.position == null) {
|
|
2498
2640
|
return;
|
|
2499
2641
|
}
|
|
2642
|
+
this.reportPlace(state.position, state.estimated_wait_seconds ?? null, state.pool_slots ?? null);
|
|
2643
|
+
}
|
|
2644
|
+
reportPlace(position, estimate, poolSlots) {
|
|
2500
2645
|
if (this._state !== "queued") {
|
|
2501
2646
|
this.setState("queued");
|
|
2502
2647
|
}
|
|
2503
2648
|
this.emit({
|
|
2504
2649
|
type: "queue-changed",
|
|
2505
|
-
position
|
|
2506
|
-
estimatedWaitSeconds: this.smoothEstimate(
|
|
2507
|
-
poolSlots
|
|
2650
|
+
position,
|
|
2651
|
+
estimatedWaitSeconds: this.smoothEstimate(position, estimate),
|
|
2652
|
+
poolSlots
|
|
2508
2653
|
});
|
|
2509
2654
|
}
|
|
2510
2655
|
smoothEstimate(position, estimate) {
|