@qontinui/ui-bridge 0.9.1 → 0.11.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/dist/index.mjs CHANGED
@@ -25799,7 +25799,7 @@ function exposeProviderOnWindow(internals) {
25799
25799
  if (typeof window === "undefined") return;
25800
25800
  const w = window;
25801
25801
  const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
25802
- root.version = "0.9.1";
25802
+ root.version = "0.11.0";
25803
25803
  root.specs = { getGlobalSpecStore };
25804
25804
  root.registry = internals.registry;
25805
25805
  root.browserCapture = internals.browserCapture;
@@ -37519,10 +37519,44 @@ function safeJsonStringify(value) {
37519
37519
  });
37520
37520
  }
37521
37521
  var HEARTBEAT_INTERVAL_MS = 1e4;
37522
+ function resolveAuthToken(authHeader) {
37523
+ if (!authHeader) return null;
37524
+ try {
37525
+ const value = authHeader();
37526
+ if (typeof value !== "string") return null;
37527
+ const trimmed = value.trim();
37528
+ return trimmed.length > 0 ? trimmed : null;
37529
+ } catch {
37530
+ return null;
37531
+ }
37532
+ }
37533
+ function transportHeaders(authHeader) {
37534
+ const headers = {
37535
+ "Content-Type": "application/json"
37536
+ };
37537
+ const token = resolveAuthToken(authHeader);
37538
+ if (token) {
37539
+ headers.Authorization = `Bearer ${token}`;
37540
+ }
37541
+ return headers;
37542
+ }
37543
+ function parseSSEDataBlock(block) {
37544
+ const dataLines = [];
37545
+ for (const line of block.split("\n")) {
37546
+ if (line.startsWith(":")) continue;
37547
+ if (!line.startsWith("data:")) continue;
37548
+ let value = line.slice(5);
37549
+ if (value.startsWith(" ")) value = value.slice(1);
37550
+ dataLines.push(value);
37551
+ }
37552
+ return dataLines.length > 0 ? dataLines.join("\n") : null;
37553
+ }
37522
37554
  function useCommandRelay(options) {
37523
37555
  const enabled = options?.enabled ?? true;
37524
37556
  const basePath = options?.basePath ?? "/api/ui-bridge";
37525
37557
  const heartbeatIntervalMs = options?.heartbeatInterval ?? HEARTBEAT_INTERVAL_MS;
37558
+ const authHeaderRef = useRef(void 0);
37559
+ authHeaderRef.current = options?.authHeader;
37526
37560
  const uiBridge = useUIBridge();
37527
37561
  const context = useUIBridgeOptional();
37528
37562
  const bridge2 = useMemo(
@@ -37555,7 +37589,6 @@ function useCommandRelay(options) {
37555
37589
  }),
37556
37590
  [uiBridge, context]
37557
37591
  );
37558
- const eventSourceRef = useRef(null);
37559
37592
  const reconnectTimeoutRef = useRef(null);
37560
37593
  const processCommandRef = useRef(() => {
37561
37594
  });
@@ -37578,7 +37611,7 @@ function useCommandRelay(options) {
37578
37611
  try {
37579
37612
  await fetch(`${basePath}/commands`, {
37580
37613
  method: "POST",
37581
- headers: { "Content-Type": "application/json" },
37614
+ headers: transportHeaders(authHeaderRef.current),
37582
37615
  body: safeJsonStringify({
37583
37616
  commandId,
37584
37617
  success: ok,
@@ -37631,10 +37664,6 @@ function useCommandRelay(options) {
37631
37664
  processCommandRef.current = processCommand;
37632
37665
  useEffect(() => {
37633
37666
  if (!enabled) {
37634
- if (eventSourceRef.current) {
37635
- eventSourceRef.current.close();
37636
- eventSourceRef.current = null;
37637
- }
37638
37667
  if (reconnectTimeoutRef.current) {
37639
37668
  clearTimeout(reconnectTimeoutRef.current);
37640
37669
  reconnectTimeoutRef.current = null;
@@ -37642,35 +37671,73 @@ function useCommandRelay(options) {
37642
37671
  return;
37643
37672
  }
37644
37673
  let isMounted = true;
37674
+ let abortController = null;
37675
+ const closeStream = () => {
37676
+ if (abortController) {
37677
+ abortController.abort();
37678
+ abortController = null;
37679
+ }
37680
+ };
37681
+ const scheduleReconnect = () => {
37682
+ if (!isMounted) return;
37683
+ if (reconnectTimeoutRef.current) return;
37684
+ reconnectTimeoutRef.current = setTimeout(() => {
37685
+ reconnectTimeoutRef.current = null;
37686
+ if (isMounted) connect();
37687
+ }, SSE_RECONNECT_DELAY_MS);
37688
+ };
37645
37689
  const connect = () => {
37646
37690
  if (!isMounted) return;
37647
- const es = new EventSource(
37648
- `${basePath}/commands/stream${tabId ? `?tabId=${encodeURIComponent(tabId)}` : ""}`
37649
- );
37650
- eventSourceRef.current = es;
37651
- es.onmessage = (event) => {
37652
- try {
37653
- const data = JSON.parse(event.data);
37654
- if (data.type === "connected") return;
37655
- if (data.commandId && data.action) processCommandRef.current(data);
37656
- } catch (e) {
37657
- console.error("[UIBridge] Failed to parse SSE message:", e);
37691
+ closeStream();
37692
+ const controller = new AbortController();
37693
+ abortController = controller;
37694
+ const url = `${basePath}/commands/stream${tabId ? `?tabId=${encodeURIComponent(tabId)}` : ""}`;
37695
+ const headers = { Accept: "text/event-stream" };
37696
+ const token = resolveAuthToken(authHeaderRef.current);
37697
+ if (token) headers.Authorization = `Bearer ${token}`;
37698
+ fetch(url, { method: "GET", headers, signal: controller.signal, cache: "no-store" }).then(async (resp) => {
37699
+ if (!resp.ok || !resp.body) {
37700
+ if (abortController === controller) abortController = null;
37701
+ scheduleReconnect();
37702
+ return;
37658
37703
  }
37659
- };
37660
- es.onerror = () => {
37661
- es.close();
37662
- if (eventSourceRef.current === es) eventSourceRef.current = null;
37663
- if (isMounted) {
37664
- reconnectTimeoutRef.current = setTimeout(() => {
37665
- reconnectTimeoutRef.current = null;
37666
- if (isMounted) connect();
37667
- }, SSE_RECONNECT_DELAY_MS);
37704
+ const reader = resp.body.getReader();
37705
+ const decoder = new TextDecoder();
37706
+ let buffer = "";
37707
+ try {
37708
+ for (; ; ) {
37709
+ const { value, done } = await reader.read();
37710
+ if (done) break;
37711
+ buffer += decoder.decode(value, { stream: true });
37712
+ let sep;
37713
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
37714
+ const block = buffer.slice(0, sep);
37715
+ buffer = buffer.slice(sep + 2);
37716
+ const data = parseSSEDataBlock(block);
37717
+ if (data == null) continue;
37718
+ try {
37719
+ const parsed = JSON.parse(data);
37720
+ if (parsed.type === "connected") continue;
37721
+ if (parsed.commandId && parsed.action) {
37722
+ processCommandRef.current(parsed);
37723
+ }
37724
+ } catch (e) {
37725
+ console.error("[UIBridge] Failed to parse SSE message:", e);
37726
+ }
37727
+ }
37728
+ }
37729
+ } catch {
37668
37730
  }
37669
- };
37731
+ if (abortController === controller) abortController = null;
37732
+ if (isMounted && !controller.signal.aborted) scheduleReconnect();
37733
+ }).catch(() => {
37734
+ if (abortController === controller) abortController = null;
37735
+ if (isMounted && !controller.signal.aborted) scheduleReconnect();
37736
+ });
37670
37737
  };
37671
37738
  const handleVisibility = () => {
37672
37739
  if (document.visibilityState === "visible") {
37673
- if (!eventSourceRef.current) {
37740
+ if (!abortController) {
37674
37741
  if (reconnectTimeoutRef.current) {
37675
37742
  clearTimeout(reconnectTimeoutRef.current);
37676
37743
  reconnectTimeoutRef.current = null;
@@ -37683,10 +37750,7 @@ function useCommandRelay(options) {
37683
37750
  document.addEventListener("visibilitychange", handleVisibility);
37684
37751
  forceReconnectRef.current = () => {
37685
37752
  if (!isMounted) return;
37686
- if (eventSourceRef.current) {
37687
- eventSourceRef.current.close();
37688
- eventSourceRef.current = null;
37689
- }
37753
+ closeStream();
37690
37754
  if (reconnectTimeoutRef.current) {
37691
37755
  clearTimeout(reconnectTimeoutRef.current);
37692
37756
  reconnectTimeoutRef.current = null;
@@ -37697,10 +37761,7 @@ function useCommandRelay(options) {
37697
37761
  isMounted = false;
37698
37762
  forceReconnectRef.current = () => {
37699
37763
  };
37700
- if (eventSourceRef.current) {
37701
- eventSourceRef.current.close();
37702
- eventSourceRef.current = null;
37703
- }
37764
+ closeStream();
37704
37765
  if (reconnectTimeoutRef.current) {
37705
37766
  clearTimeout(reconnectTimeoutRef.current);
37706
37767
  reconnectTimeoutRef.current = null;
@@ -37733,7 +37794,7 @@ function useCommandRelay(options) {
37733
37794
  if (heartbeatVersion !== void 0) body.version = heartbeatVersion;
37734
37795
  const resp = await fetch(`${basePath}/heartbeat`, {
37735
37796
  method: "POST",
37736
- headers: { "Content-Type": "application/json" },
37797
+ headers: transportHeaders(authHeaderRef.current),
37737
37798
  body: JSON.stringify(body)
37738
37799
  });
37739
37800
  if (resp.ok) {
@@ -37864,7 +37925,8 @@ function CommandRelayListener(props) {
37864
37925
  appType: props.appType,
37865
37926
  framework: props.framework,
37866
37927
  capabilities: props.capabilities,
37867
- version: props.version
37928
+ version: props.version,
37929
+ authHeader: props.authHeader
37868
37930
  };
37869
37931
  useCommandRelay(options);
37870
37932
  return null;