@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.
@@ -24946,7 +24946,7 @@ function exposeProviderOnWindow(internals) {
24946
24946
  if (typeof window === "undefined") return;
24947
24947
  const w = window;
24948
24948
  const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
24949
- root.version = "0.9.1";
24949
+ root.version = "0.11.0";
24950
24950
  root.specs = { getGlobalSpecStore };
24951
24951
  root.registry = internals.registry;
24952
24952
  root.browserCapture = internals.browserCapture;
@@ -32909,10 +32909,44 @@ function safeJsonStringify(value) {
32909
32909
  });
32910
32910
  }
32911
32911
  var HEARTBEAT_INTERVAL_MS = 1e4;
32912
+ function resolveAuthToken(authHeader) {
32913
+ if (!authHeader) return null;
32914
+ try {
32915
+ const value = authHeader();
32916
+ if (typeof value !== "string") return null;
32917
+ const trimmed = value.trim();
32918
+ return trimmed.length > 0 ? trimmed : null;
32919
+ } catch {
32920
+ return null;
32921
+ }
32922
+ }
32923
+ function transportHeaders(authHeader) {
32924
+ const headers = {
32925
+ "Content-Type": "application/json"
32926
+ };
32927
+ const token = resolveAuthToken(authHeader);
32928
+ if (token) {
32929
+ headers.Authorization = `Bearer ${token}`;
32930
+ }
32931
+ return headers;
32932
+ }
32933
+ function parseSSEDataBlock(block) {
32934
+ const dataLines = [];
32935
+ for (const line of block.split("\n")) {
32936
+ if (line.startsWith(":")) continue;
32937
+ if (!line.startsWith("data:")) continue;
32938
+ let value = line.slice(5);
32939
+ if (value.startsWith(" ")) value = value.slice(1);
32940
+ dataLines.push(value);
32941
+ }
32942
+ return dataLines.length > 0 ? dataLines.join("\n") : null;
32943
+ }
32912
32944
  function useCommandRelay(options) {
32913
32945
  const enabled = options?.enabled ?? true;
32914
32946
  const basePath = options?.basePath ?? "/api/ui-bridge";
32915
32947
  const heartbeatIntervalMs = options?.heartbeatInterval ?? HEARTBEAT_INTERVAL_MS;
32948
+ const authHeaderRef = useRef(void 0);
32949
+ authHeaderRef.current = options?.authHeader;
32916
32950
  const uiBridge = useUIBridge();
32917
32951
  const context = useUIBridgeOptional();
32918
32952
  const bridge2 = useMemo(
@@ -32945,7 +32979,6 @@ function useCommandRelay(options) {
32945
32979
  }),
32946
32980
  [uiBridge, context]
32947
32981
  );
32948
- const eventSourceRef = useRef(null);
32949
32982
  const reconnectTimeoutRef = useRef(null);
32950
32983
  const processCommandRef = useRef(() => {
32951
32984
  });
@@ -32968,7 +33001,7 @@ function useCommandRelay(options) {
32968
33001
  try {
32969
33002
  await fetch(`${basePath}/commands`, {
32970
33003
  method: "POST",
32971
- headers: { "Content-Type": "application/json" },
33004
+ headers: transportHeaders(authHeaderRef.current),
32972
33005
  body: safeJsonStringify({
32973
33006
  commandId,
32974
33007
  success: ok,
@@ -33021,10 +33054,6 @@ function useCommandRelay(options) {
33021
33054
  processCommandRef.current = processCommand;
33022
33055
  useEffect(() => {
33023
33056
  if (!enabled) {
33024
- if (eventSourceRef.current) {
33025
- eventSourceRef.current.close();
33026
- eventSourceRef.current = null;
33027
- }
33028
33057
  if (reconnectTimeoutRef.current) {
33029
33058
  clearTimeout(reconnectTimeoutRef.current);
33030
33059
  reconnectTimeoutRef.current = null;
@@ -33032,35 +33061,73 @@ function useCommandRelay(options) {
33032
33061
  return;
33033
33062
  }
33034
33063
  let isMounted = true;
33064
+ let abortController = null;
33065
+ const closeStream = () => {
33066
+ if (abortController) {
33067
+ abortController.abort();
33068
+ abortController = null;
33069
+ }
33070
+ };
33071
+ const scheduleReconnect = () => {
33072
+ if (!isMounted) return;
33073
+ if (reconnectTimeoutRef.current) return;
33074
+ reconnectTimeoutRef.current = setTimeout(() => {
33075
+ reconnectTimeoutRef.current = null;
33076
+ if (isMounted) connect();
33077
+ }, SSE_RECONNECT_DELAY_MS);
33078
+ };
33035
33079
  const connect = () => {
33036
33080
  if (!isMounted) return;
33037
- const es = new EventSource(
33038
- `${basePath}/commands/stream${tabId ? `?tabId=${encodeURIComponent(tabId)}` : ""}`
33039
- );
33040
- eventSourceRef.current = es;
33041
- es.onmessage = (event) => {
33042
- try {
33043
- const data = JSON.parse(event.data);
33044
- if (data.type === "connected") return;
33045
- if (data.commandId && data.action) processCommandRef.current(data);
33046
- } catch (e) {
33047
- console.error("[UIBridge] Failed to parse SSE message:", e);
33081
+ closeStream();
33082
+ const controller = new AbortController();
33083
+ abortController = controller;
33084
+ const url = `${basePath}/commands/stream${tabId ? `?tabId=${encodeURIComponent(tabId)}` : ""}`;
33085
+ const headers = { Accept: "text/event-stream" };
33086
+ const token = resolveAuthToken(authHeaderRef.current);
33087
+ if (token) headers.Authorization = `Bearer ${token}`;
33088
+ fetch(url, { method: "GET", headers, signal: controller.signal, cache: "no-store" }).then(async (resp) => {
33089
+ if (!resp.ok || !resp.body) {
33090
+ if (abortController === controller) abortController = null;
33091
+ scheduleReconnect();
33092
+ return;
33048
33093
  }
33049
- };
33050
- es.onerror = () => {
33051
- es.close();
33052
- if (eventSourceRef.current === es) eventSourceRef.current = null;
33053
- if (isMounted) {
33054
- reconnectTimeoutRef.current = setTimeout(() => {
33055
- reconnectTimeoutRef.current = null;
33056
- if (isMounted) connect();
33057
- }, SSE_RECONNECT_DELAY_MS);
33094
+ const reader = resp.body.getReader();
33095
+ const decoder = new TextDecoder();
33096
+ let buffer = "";
33097
+ try {
33098
+ for (; ; ) {
33099
+ const { value, done } = await reader.read();
33100
+ if (done) break;
33101
+ buffer += decoder.decode(value, { stream: true });
33102
+ let sep;
33103
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
33104
+ const block = buffer.slice(0, sep);
33105
+ buffer = buffer.slice(sep + 2);
33106
+ const data = parseSSEDataBlock(block);
33107
+ if (data == null) continue;
33108
+ try {
33109
+ const parsed = JSON.parse(data);
33110
+ if (parsed.type === "connected") continue;
33111
+ if (parsed.commandId && parsed.action) {
33112
+ processCommandRef.current(parsed);
33113
+ }
33114
+ } catch (e) {
33115
+ console.error("[UIBridge] Failed to parse SSE message:", e);
33116
+ }
33117
+ }
33118
+ }
33119
+ } catch {
33058
33120
  }
33059
- };
33121
+ if (abortController === controller) abortController = null;
33122
+ if (isMounted && !controller.signal.aborted) scheduleReconnect();
33123
+ }).catch(() => {
33124
+ if (abortController === controller) abortController = null;
33125
+ if (isMounted && !controller.signal.aborted) scheduleReconnect();
33126
+ });
33060
33127
  };
33061
33128
  const handleVisibility = () => {
33062
33129
  if (document.visibilityState === "visible") {
33063
- if (!eventSourceRef.current) {
33130
+ if (!abortController) {
33064
33131
  if (reconnectTimeoutRef.current) {
33065
33132
  clearTimeout(reconnectTimeoutRef.current);
33066
33133
  reconnectTimeoutRef.current = null;
@@ -33073,10 +33140,7 @@ function useCommandRelay(options) {
33073
33140
  document.addEventListener("visibilitychange", handleVisibility);
33074
33141
  forceReconnectRef.current = () => {
33075
33142
  if (!isMounted) return;
33076
- if (eventSourceRef.current) {
33077
- eventSourceRef.current.close();
33078
- eventSourceRef.current = null;
33079
- }
33143
+ closeStream();
33080
33144
  if (reconnectTimeoutRef.current) {
33081
33145
  clearTimeout(reconnectTimeoutRef.current);
33082
33146
  reconnectTimeoutRef.current = null;
@@ -33087,10 +33151,7 @@ function useCommandRelay(options) {
33087
33151
  isMounted = false;
33088
33152
  forceReconnectRef.current = () => {
33089
33153
  };
33090
- if (eventSourceRef.current) {
33091
- eventSourceRef.current.close();
33092
- eventSourceRef.current = null;
33093
- }
33154
+ closeStream();
33094
33155
  if (reconnectTimeoutRef.current) {
33095
33156
  clearTimeout(reconnectTimeoutRef.current);
33096
33157
  reconnectTimeoutRef.current = null;
@@ -33123,7 +33184,7 @@ function useCommandRelay(options) {
33123
33184
  if (heartbeatVersion !== void 0) body.version = heartbeatVersion;
33124
33185
  const resp = await fetch(`${basePath}/heartbeat`, {
33125
33186
  method: "POST",
33126
- headers: { "Content-Type": "application/json" },
33187
+ headers: transportHeaders(authHeaderRef.current),
33127
33188
  body: JSON.stringify(body)
33128
33189
  });
33129
33190
  if (resp.ok) {
@@ -33254,7 +33315,8 @@ function CommandRelayListener(props) {
33254
33315
  appType: props.appType,
33255
33316
  framework: props.framework,
33256
33317
  capabilities: props.capabilities,
33257
- version: props.version
33318
+ version: props.version,
33319
+ authHeader: props.authHeader
33258
33320
  };
33259
33321
  useCommandRelay(options);
33260
33322
  return null;