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