@crestapps/ai-chat-ui 2.0.0-preview.170 → 2.0.0-preview.172

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.
@@ -762,6 +762,13 @@
762
762
  // network where WebRTC cannot work pays the full connection timeout — and a second microphone prompt —
763
763
  // at the start of every single conversation.
764
764
  var REALTIME_WEBRTC_BLOCKED_KEY = 'coreai.realtime.webrtcBlocked';
765
+ // The ICE servers resolved for the conversation currently starting. Scoped to one attempt, not to the page:
766
+ // TURN credentials are short-lived, so every conversation resolves its own set — but within an attempt the
767
+ // transport decision and the peer must see the same servers, and one hub round-trip is enough.
768
+ var realtimeAttemptIceServers = null;
769
+ // Identifies the conversation currently starting, so an ICE resolution that lands after the user gave up
770
+ // (or started again) cannot open a session for an attempt that no longer exists.
771
+ var realtimeAttemptToken = 0;
765
772
  var realtimeSawRelayCandidate = false;
766
773
  // WebRTC half-duplex echo guard: the shared microphone gate (see createMicGate) watches both the mic and
767
774
  // the assistant's remote audio and silences the outbound track unless the user is genuinely speaking, so
@@ -1441,28 +1448,63 @@
1441
1448
  if (webRtcIceServers) {
1442
1449
  return Promise.resolve(webRtcIceServers);
1443
1450
  }
1451
+ if (realtimeAttemptIceServers) {
1452
+ return Promise.resolve(realtimeAttemptIceServers);
1453
+ }
1444
1454
  if (!connection || typeof connection.invoke !== 'function') {
1445
1455
  return Promise.resolve(DEFAULT_ICE_SERVERS);
1446
1456
  }
1447
1457
  return connection.invoke('GetRealtimeIceServers').then(function (servers) {
1448
- return Array.isArray(servers) && servers.length ? servers : DEFAULT_ICE_SERVERS;
1458
+ realtimeAttemptIceServers = Array.isArray(servers) && servers.length ? servers : DEFAULT_ICE_SERVERS;
1459
+ return realtimeAttemptIceServers;
1449
1460
  })["catch"](function (err) {
1450
1461
  if (window.console && console.warn) {
1451
1462
  console.warn('Could not resolve the realtime ICE servers; using the default STUN server.', err);
1452
1463
  }
1453
- return DEFAULT_ICE_SERVERS;
1464
+ realtimeAttemptIceServers = DEFAULT_ICE_SERVERS;
1465
+ return realtimeAttemptIceServers;
1454
1466
  });
1455
1467
  }
1456
- function isWebRtcKnownBlocked() {
1468
+
1469
+ // Identifies an ICE configuration by its server URLs, deliberately ignoring usernames and credentials:
1470
+ // ephemeral TURN credentials (Cloudflare, coturn HMAC) are reissued for every session, so including them
1471
+ // would change the fingerprint on each attempt and make the remembered failure worthless. Adding, removing
1472
+ // or repointing a STUN/TURN server does change it — which is the case that must invalidate the memory.
1473
+ function iceServersFingerprint(servers) {
1474
+ var urls = [];
1457
1475
  try {
1458
- return window.sessionStorage.getItem(REALTIME_WEBRTC_BLOCKED_KEY) === '1';
1476
+ (servers || []).forEach(function (server) {
1477
+ var list = server && server.urls;
1478
+ if (typeof list === 'string') {
1479
+ urls.push(list);
1480
+ } else if (Array.isArray(list)) {
1481
+ list.forEach(function (url) {
1482
+ if (url) {
1483
+ urls.push(url);
1484
+ }
1485
+ });
1486
+ }
1487
+ });
1488
+ } catch (err) {
1489
+ return 'none';
1490
+ }
1491
+ return urls.length ? urls.sort().join('|') : 'none';
1492
+ }
1493
+
1494
+ // A remembered failure only applies to the ICE configuration that produced it. Anything else strands a
1495
+ // deployment that has since been given a TURN server on the WebSocket transport until every open tab is
1496
+ // closed, with no way for the user to tell why. Values written by older builds ('1') match no fingerprint,
1497
+ // so upgrading clears the flag on its own.
1498
+ function isWebRtcKnownBlocked(servers) {
1499
+ try {
1500
+ return window.sessionStorage.getItem(REALTIME_WEBRTC_BLOCKED_KEY) === iceServersFingerprint(servers);
1459
1501
  } catch (err) {
1460
1502
  return false;
1461
1503
  }
1462
1504
  }
1463
1505
  function rememberWebRtcBlocked() {
1464
1506
  try {
1465
- window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY, '1');
1507
+ window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY, iceServersFingerprint(realtimeAttemptIceServers || webRtcIceServers));
1466
1508
  } catch (err) {}
1467
1509
  }
1468
1510
 
@@ -1503,6 +1545,8 @@
1503
1545
  return;
1504
1546
  }
1505
1547
  applyRealtimeAudioPrefs(loadRealtimeAudioPrefs());
1548
+ realtimeAttemptIceServers = null;
1549
+ var attempt = ++realtimeAttemptToken;
1506
1550
  realtimeFellBack = false;
1507
1551
  realtimeSessionReady = false;
1508
1552
  realtimeEndedNotice = null;
@@ -1510,20 +1554,34 @@
1510
1554
  bindRealtimeLifecycleHandlers();
1511
1555
  setRealtimeState('requesting-mic');
1512
1556
 
1557
+ // Starting on WebSocket without even attempting WebRTC is a deployment fact worth stating once. The
1558
+ // console is otherwise indistinguishable from a healthy WebRTC session, so "no warning" gets read as
1559
+ // "WebRTC is working" when it can equally mean the server never offered the transport at all.
1560
+ if (!webRtcEnabled) {
1561
+ logWebSocketTransportReason('the server did not advertise the WebRTC transport');
1562
+ startRealtimeWebSocketConversation();
1563
+ return;
1564
+ }
1565
+
1513
1566
  // Prefer the WebRTC transport when the server advertises it: the browser's echo canceller references
1514
1567
  // the assistant's media track, so the model can ignore its own voice with the mic open (open rooms).
1515
1568
  // If the peer cannot connect (blocked UDP, no TURN, unsupported), we fall back to WebSocket at connect
1516
1569
  // time — see fallbackToWebSocket. The decision is made once, before the session starts.
1517
- if (webRtcEnabled && !isWebRtcKnownBlocked()) {
1570
+ //
1571
+ // Resolving the ICE servers before choosing a transport costs no extra round-trip — the peer reuses
1572
+ // realtimeAttemptIceServers — and it is what lets a remembered failure be scoped to the configuration
1573
+ // that actually caused it.
1574
+ resolveIceServers().then(function (servers) {
1575
+ if (attempt !== realtimeAttemptToken || !isRealtimeMode || isRealtimeActive) {
1576
+ return;
1577
+ }
1578
+ if (isWebRtcKnownBlocked(servers)) {
1579
+ logWebSocketTransportReason('a WebRTC attempt already failed earlier in this browser session with the same ICE configuration');
1580
+ startRealtimeWebSocketConversation();
1581
+ return;
1582
+ }
1518
1583
  startRealtimeWebRtcConversation();
1519
- return;
1520
- }
1521
-
1522
- // Starting on WebSocket without even attempting WebRTC is a deployment fact worth stating once. The
1523
- // console is otherwise indistinguishable from a healthy WebRTC session, so "no warning" gets read as
1524
- // "WebRTC is working" when it can equally mean the server never offered the transport at all.
1525
- logWebSocketTransportReason(webRtcEnabled ? 'a WebRTC attempt already failed earlier in this browser session' : 'the server did not advertise the WebRTC transport');
1526
- startRealtimeWebSocketConversation();
1584
+ });
1527
1585
  }
1528
1586
  function startRealtimeWebSocketConversation() {
1529
1587
  if (webRtcEnabled) {