@qontinui/ui-bridge 0.10.0 → 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.
@@ -1623,9 +1623,10 @@ interface UseCommandRelayOptions {
1623
1623
  *
1624
1624
  * - attaches `Authorization: Bearer <value>` to outbound
1625
1625
  * `POST {basePath}/commands` and `POST {basePath}/heartbeat`;
1626
- * - appends a `_auth=<value>` query parameter to the SSE URL
1627
- * `GET {basePath}/commands/stream` (EventSource cannot set custom
1628
- * headers per the WhatWG spec).
1626
+ * - attaches `Authorization: Bearer <value>` to the SSE
1627
+ * `GET {basePath}/commands/stream` request as well — the stream is
1628
+ * consumed via `fetch` streaming, so the header rides on the request
1629
+ * (no token in the URL).
1629
1630
  *
1630
1631
  * Called fresh on every outbound request — implementations should read
1631
1632
  * the token from a live source (e.g. `sessionStorage`) so a token
@@ -1623,9 +1623,10 @@ interface UseCommandRelayOptions {
1623
1623
  *
1624
1624
  * - attaches `Authorization: Bearer <value>` to outbound
1625
1625
  * `POST {basePath}/commands` and `POST {basePath}/heartbeat`;
1626
- * - appends a `_auth=<value>` query parameter to the SSE URL
1627
- * `GET {basePath}/commands/stream` (EventSource cannot set custom
1628
- * headers per the WhatWG spec).
1626
+ * - attaches `Authorization: Bearer <value>` to the SSE
1627
+ * `GET {basePath}/commands/stream` request as well — the stream is
1628
+ * consumed via `fetch` streaming, so the header rides on the request
1629
+ * (no token in the URL).
1629
1630
  *
1630
1631
  * Called fresh on every outbound request — implementations should read
1631
1632
  * the token from a live source (e.g. `sessionStorage`) so a token
@@ -24948,7 +24948,7 @@ function exposeProviderOnWindow(internals) {
24948
24948
  if (typeof window === "undefined") return;
24949
24949
  const w = window;
24950
24950
  const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
24951
- root.version = "0.10.0";
24951
+ root.version = "0.11.0";
24952
24952
  root.specs = { getGlobalSpecStore };
24953
24953
  root.registry = internals.registry;
24954
24954
  root.browserCapture = internals.browserCapture;
@@ -32932,11 +32932,16 @@ function transportHeaders(authHeader) {
32932
32932
  }
32933
32933
  return headers;
32934
32934
  }
32935
- function appendAuthQuery(url, authHeader) {
32936
- const token = resolveAuthToken(authHeader);
32937
- if (!token) return url;
32938
- const sep = url.includes("?") ? "&" : "?";
32939
- return `${url}${sep}_auth=${encodeURIComponent(token)}`;
32935
+ function parseSSEDataBlock(block) {
32936
+ const dataLines = [];
32937
+ for (const line of block.split("\n")) {
32938
+ if (line.startsWith(":")) continue;
32939
+ if (!line.startsWith("data:")) continue;
32940
+ let value = line.slice(5);
32941
+ if (value.startsWith(" ")) value = value.slice(1);
32942
+ dataLines.push(value);
32943
+ }
32944
+ return dataLines.length > 0 ? dataLines.join("\n") : null;
32940
32945
  }
32941
32946
  function useCommandRelay(options) {
32942
32947
  const enabled = options?.enabled ?? true;
@@ -32976,7 +32981,6 @@ function useCommandRelay(options) {
32976
32981
  }),
32977
32982
  [uiBridge, context]
32978
32983
  );
32979
- const eventSourceRef = react.useRef(null);
32980
32984
  const reconnectTimeoutRef = react.useRef(null);
32981
32985
  const processCommandRef = react.useRef(() => {
32982
32986
  });
@@ -33052,10 +33056,6 @@ function useCommandRelay(options) {
33052
33056
  processCommandRef.current = processCommand;
33053
33057
  react.useEffect(() => {
33054
33058
  if (!enabled) {
33055
- if (eventSourceRef.current) {
33056
- eventSourceRef.current.close();
33057
- eventSourceRef.current = null;
33058
- }
33059
33059
  if (reconnectTimeoutRef.current) {
33060
33060
  clearTimeout(reconnectTimeoutRef.current);
33061
33061
  reconnectTimeoutRef.current = null;
@@ -33063,34 +33063,73 @@ function useCommandRelay(options) {
33063
33063
  return;
33064
33064
  }
33065
33065
  let isMounted = true;
33066
+ let abortController = null;
33067
+ const closeStream = () => {
33068
+ if (abortController) {
33069
+ abortController.abort();
33070
+ abortController = null;
33071
+ }
33072
+ };
33073
+ const scheduleReconnect = () => {
33074
+ if (!isMounted) return;
33075
+ if (reconnectTimeoutRef.current) return;
33076
+ reconnectTimeoutRef.current = setTimeout(() => {
33077
+ reconnectTimeoutRef.current = null;
33078
+ if (isMounted) connect();
33079
+ }, SSE_RECONNECT_DELAY_MS);
33080
+ };
33066
33081
  const connect = () => {
33067
33082
  if (!isMounted) return;
33068
- const baseUrl = `${basePath}/commands/stream${tabId ? `?tabId=${encodeURIComponent(tabId)}` : ""}`;
33069
- const es = new EventSource(appendAuthQuery(baseUrl, authHeaderRef.current));
33070
- eventSourceRef.current = es;
33071
- es.onmessage = (event) => {
33072
- try {
33073
- const data = JSON.parse(event.data);
33074
- if (data.type === "connected") return;
33075
- if (data.commandId && data.action) processCommandRef.current(data);
33076
- } catch (e) {
33077
- console.error("[UIBridge] Failed to parse SSE message:", e);
33083
+ closeStream();
33084
+ const controller = new AbortController();
33085
+ abortController = controller;
33086
+ const url = `${basePath}/commands/stream${tabId ? `?tabId=${encodeURIComponent(tabId)}` : ""}`;
33087
+ const headers = { Accept: "text/event-stream" };
33088
+ const token = resolveAuthToken(authHeaderRef.current);
33089
+ if (token) headers.Authorization = `Bearer ${token}`;
33090
+ fetch(url, { method: "GET", headers, signal: controller.signal, cache: "no-store" }).then(async (resp) => {
33091
+ if (!resp.ok || !resp.body) {
33092
+ if (abortController === controller) abortController = null;
33093
+ scheduleReconnect();
33094
+ return;
33078
33095
  }
33079
- };
33080
- es.onerror = () => {
33081
- es.close();
33082
- if (eventSourceRef.current === es) eventSourceRef.current = null;
33083
- if (isMounted) {
33084
- reconnectTimeoutRef.current = setTimeout(() => {
33085
- reconnectTimeoutRef.current = null;
33086
- if (isMounted) connect();
33087
- }, SSE_RECONNECT_DELAY_MS);
33096
+ const reader = resp.body.getReader();
33097
+ const decoder = new TextDecoder();
33098
+ let buffer = "";
33099
+ try {
33100
+ for (; ; ) {
33101
+ const { value, done } = await reader.read();
33102
+ if (done) break;
33103
+ buffer += decoder.decode(value, { stream: true });
33104
+ let sep;
33105
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
33106
+ const block = buffer.slice(0, sep);
33107
+ buffer = buffer.slice(sep + 2);
33108
+ const data = parseSSEDataBlock(block);
33109
+ if (data == null) continue;
33110
+ try {
33111
+ const parsed = JSON.parse(data);
33112
+ if (parsed.type === "connected") continue;
33113
+ if (parsed.commandId && parsed.action) {
33114
+ processCommandRef.current(parsed);
33115
+ }
33116
+ } catch (e) {
33117
+ console.error("[UIBridge] Failed to parse SSE message:", e);
33118
+ }
33119
+ }
33120
+ }
33121
+ } catch {
33088
33122
  }
33089
- };
33123
+ if (abortController === controller) abortController = null;
33124
+ if (isMounted && !controller.signal.aborted) scheduleReconnect();
33125
+ }).catch(() => {
33126
+ if (abortController === controller) abortController = null;
33127
+ if (isMounted && !controller.signal.aborted) scheduleReconnect();
33128
+ });
33090
33129
  };
33091
33130
  const handleVisibility = () => {
33092
33131
  if (document.visibilityState === "visible") {
33093
- if (!eventSourceRef.current) {
33132
+ if (!abortController) {
33094
33133
  if (reconnectTimeoutRef.current) {
33095
33134
  clearTimeout(reconnectTimeoutRef.current);
33096
33135
  reconnectTimeoutRef.current = null;
@@ -33103,10 +33142,7 @@ function useCommandRelay(options) {
33103
33142
  document.addEventListener("visibilitychange", handleVisibility);
33104
33143
  forceReconnectRef.current = () => {
33105
33144
  if (!isMounted) return;
33106
- if (eventSourceRef.current) {
33107
- eventSourceRef.current.close();
33108
- eventSourceRef.current = null;
33109
- }
33145
+ closeStream();
33110
33146
  if (reconnectTimeoutRef.current) {
33111
33147
  clearTimeout(reconnectTimeoutRef.current);
33112
33148
  reconnectTimeoutRef.current = null;
@@ -33117,10 +33153,7 @@ function useCommandRelay(options) {
33117
33153
  isMounted = false;
33118
33154
  forceReconnectRef.current = () => {
33119
33155
  };
33120
- if (eventSourceRef.current) {
33121
- eventSourceRef.current.close();
33122
- eventSourceRef.current = null;
33123
- }
33156
+ closeStream();
33124
33157
  if (reconnectTimeoutRef.current) {
33125
33158
  clearTimeout(reconnectTimeoutRef.current);
33126
33159
  reconnectTimeoutRef.current = null;