@qontinui/ui-bridge-native 0.6.9 → 0.6.10

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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Resilient TCP-bind helper for native UI Bridge server adapters.
3
+ *
4
+ * Background: the on-device bridge binds a TCP server on a fixed port (8087).
5
+ * On hot-reload, app foreground/background cycles, or a crash-restart, the OS
6
+ * can leave the previous socket in a lingering state, so the fresh bind fails
7
+ * with `EADDRINUSE`. The original adapter gave up after 3 fixed retries
8
+ * (~5s total), after which the bridge was permanently dead until the app was
9
+ * fully restarted — a single transient stale socket bricked automation.
10
+ *
11
+ * This helper centralises a robust bind loop so every adapter (and a unit
12
+ * test) shares one implementation:
13
+ * - SO_REUSEADDR is requested on bind where the platform/library supports it
14
+ * (passed through to the caller's `attempt` via `reuseAddress: true`), so
15
+ * a lingering TIME_WAIT socket doesn't block rebind.
16
+ * - On `EADDRINUSE` it retries with exponential backoff (capped) for at
17
+ * least `minTotalMs` (default 30s) instead of bailing after a fixed count.
18
+ * - Every retry is logged so operators can see the bridge recovering.
19
+ * - Non-`EADDRINUSE` errors fail fast (a wrong-permission / bad-host error
20
+ * won't fix itself by waiting).
21
+ *
22
+ * The helper is transport-agnostic: the caller supplies an `attempt` thunk
23
+ * that performs ONE bind attempt and resolves on `listening` / rejects on
24
+ * error. This keeps the package free of any concrete TCP library dependency
25
+ * (the same reason `ServerAdapter` is an interface).
26
+ */
27
+ /** True when an error looks like an address-already-in-use bind failure. */
28
+ export declare function isAddrInUseError(err: unknown): boolean;
29
+ export interface BindWithRetryOptions {
30
+ /**
31
+ * Minimum total wall-clock time to keep retrying `EADDRINUSE` before giving
32
+ * up, in milliseconds. The loop always makes at least one attempt and keeps
33
+ * retrying until this budget is exhausted. Default 30_000 (30s).
34
+ *
35
+ * Pass `Infinity` to retry indefinitely (with capped backoff) — appropriate
36
+ * for an always-on device bridge that should self-heal whenever the stale
37
+ * socket finally clears.
38
+ */
39
+ minTotalMs?: number;
40
+ /** Initial backoff delay between retries, in ms. Default 500. */
41
+ initialDelayMs?: number;
42
+ /** Maximum backoff delay (cap for exponential growth), in ms. Default 5_000. */
43
+ maxDelayMs?: number;
44
+ /** Multiplier applied to the delay after each retry. Default 2. */
45
+ backoffFactor?: number;
46
+ /** Logger for retry diagnostics. Defaults to `console`. */
47
+ logger?: Pick<Console, 'warn' | 'error'>;
48
+ /** Injectable sleep — overridable in tests to avoid real timers. */
49
+ sleep?: (ms: number) => Promise<void>;
50
+ /** Injectable clock — overridable in tests. Defaults to `Date.now`. */
51
+ now?: () => number;
52
+ }
53
+ /**
54
+ * Run `attempt()` repeatedly until it resolves, retrying on `EADDRINUSE` with
55
+ * exponential backoff for at least `minTotalMs`. Non-address-in-use errors
56
+ * reject immediately. Resolves once a bind attempt succeeds.
57
+ *
58
+ * @param attempt One bind attempt. Receives the attempt index (0-based) and
59
+ * MUST resolve on a successful `listening` event or reject on error. The
60
+ * caller is responsible for tearing down any half-bound server before the
61
+ * next attempt (see the adapter's `recreate` thunk).
62
+ */
63
+ export declare function bindWithRetry(attempt: (attemptIndex: number) => Promise<void>, options?: BindWithRetryOptions): Promise<void>;
64
+ //# sourceMappingURL=bind-retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-retry.d.ts","sourceRoot":"","sources":["../../src/server/bind-retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAWtD;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IACzC,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAKD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EAChD,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAwCf"}
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAQV,oBAAoB,EAKrB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAyThF;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,sBAAsB,EAChC,QAAQ,EAAE,oBAAoB,EAC9B,MAAM,CAAC,EAAE;IACP,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,MAAM;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D,GACA,oBAAoB,CAg8BtB"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAQV,oBAAoB,EAKrB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AA4VhF;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,sBAAsB,EAChC,QAAQ,EAAE,oBAAoB,EAC9B,MAAM,CAAC,EAAE;IACP,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,MAAM;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D,GACA,oBAAoB,CA09BtB"}
@@ -13,4 +13,6 @@ export { WebSocketConnection } from './ws-connection';
13
13
  export type { WebSocketMessageHandler } from './ws-connection';
14
14
  export { WebSocketEventBridge } from './ws-event-bridge';
15
15
  export { ConsoleErrorBuffer, NetworkRequestBuffer } from './observability';
16
+ export { bindWithRetry, isAddrInUseError } from './bind-retry';
17
+ export type { BindWithRetryOptions } from './bind-retry';
16
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
@@ -960,6 +960,13 @@ function matchElementsByText(registry, text, opts) {
960
960
  scored.sort((a, b) => a.rank - b.rank);
961
961
  return scored.map((s) => s.element);
962
962
  }
963
+ function resolveElementState(element) {
964
+ const state = element.getState();
965
+ if (element.type === "input" && state.value === void 0 && typeof element.props?.value === "string") {
966
+ return { ...state, value: element.props.value };
967
+ }
968
+ return state;
969
+ }
963
970
  async function executeWorkflowStep(step, registry, executor) {
964
971
  const startTime = Date.now();
965
972
  try {
@@ -1196,7 +1203,7 @@ function createServerHandlers(registry, executor, config) {
1196
1203
  type: element.type,
1197
1204
  label: element.label,
1198
1205
  identifier: element.getIdentifier(),
1199
- state: element.getState(),
1206
+ state: resolveElementState(element),
1200
1207
  actions: element.actions,
1201
1208
  customActions: element.customActions ? Object.keys(element.customActions) : void 0,
1202
1209
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
@@ -1210,18 +1217,19 @@ function createServerHandlers(registry, executor, config) {
1210
1217
  if (!element) {
1211
1218
  return error2(`Element not found: ${id}`, "ELEMENT_NOT_FOUND");
1212
1219
  }
1213
- return success2({ state: element.getState() });
1220
+ return success2({ state: resolveElementState(element) });
1214
1221
  },
1215
1222
  executeAction: async (ctx) => {
1216
- const { id } = ctx.params;
1217
- const body = ctx.body;
1218
- if (!body?.action) {
1223
+ const rawBody = ctx.body ?? {};
1224
+ const envelope = rawBody.request && typeof rawBody.request === "object" ? rawBody.request : rawBody;
1225
+ const id = ctx.params.id ?? rawBody.id ?? "";
1226
+ if (!envelope?.action) {
1219
1227
  return error2("Action is required", "INVALID_REQUEST");
1220
1228
  }
1221
1229
  const response = await executor.executeAction(id, {
1222
- action: body.action,
1223
- params: body.params,
1224
- waitOptions: body.waitOptions
1230
+ action: envelope.action,
1231
+ params: envelope.params,
1232
+ waitOptions: envelope.waitOptions
1225
1233
  });
1226
1234
  if (!response.success) {
1227
1235
  return error2(response.error || "Action failed", "ACTION_FAILED");
@@ -1860,7 +1868,7 @@ function createServerHandlers(registry, executor, config) {
1860
1868
  };
1861
1869
  if (config?.appInfo) {
1862
1870
  response.uiBridge = {
1863
- version: "0.3.0",
1871
+ version: "0.6.10",
1864
1872
  ...config.appInfo,
1865
1873
  capabilities: ["elements", "components", "actions", "design"],
1866
1874
  elementCount: stats.elements,
@@ -3986,6 +3994,46 @@ var WebSocketConnection = class {
3986
3994
  }
3987
3995
  };
3988
3996
 
3997
+ // src/server/bind-retry.ts
3998
+ function isAddrInUseError(err) {
3999
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
4000
+ const code = err?.code;
4001
+ return message.includes("EADDRINUSE") || message.toLowerCase().includes("address already in use") || code === "EADDRINUSE";
4002
+ }
4003
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4004
+ async function bindWithRetry(attempt, options = {}) {
4005
+ const {
4006
+ minTotalMs = 3e4,
4007
+ initialDelayMs = 500,
4008
+ maxDelayMs = 5e3,
4009
+ backoffFactor = 2,
4010
+ logger = console,
4011
+ sleep = defaultSleep,
4012
+ now = Date.now
4013
+ } = options;
4014
+ const startedAt = now();
4015
+ let delay = initialDelayMs;
4016
+ let attemptIndex = 0;
4017
+ for (; ; ) {
4018
+ try {
4019
+ await attempt(attemptIndex);
4020
+ return;
4021
+ } catch (err) {
4022
+ const elapsed = now() - startedAt;
4023
+ const budgetExhausted = elapsed + delay >= minTotalMs;
4024
+ if (!isAddrInUseError(err) || budgetExhausted) {
4025
+ throw err;
4026
+ }
4027
+ logger.warn(
4028
+ `[ui-bridge-native] bind failed with EADDRINUSE (attempt ${attemptIndex + 1}); retrying in ${delay}ms (elapsed ${elapsed}ms / budget ${minTotalMs === Infinity ? "\u221E" : minTotalMs}ms)`
4029
+ );
4030
+ await sleep(delay);
4031
+ delay = Math.min(delay * backoffFactor, maxDelayMs);
4032
+ attemptIndex += 1;
4033
+ }
4034
+ }
4035
+ }
4036
+
3989
4037
  exports.ConsoleErrorBuffer = ConsoleErrorBuffer;
3990
4038
  exports.NativeUIBridgeServer = NativeUIBridgeServer;
3991
4039
  exports.NetworkRequestBuffer = NetworkRequestBuffer;
@@ -3998,6 +4046,7 @@ exports.OPCODE_TEXT = OPCODE_TEXT;
3998
4046
  exports.UI_BRIDGE_NATIVE_ROUTES = UI_BRIDGE_NATIVE_ROUTES;
3999
4047
  exports.WebSocketConnection = WebSocketConnection;
4000
4048
  exports.WebSocketEventBridge = WebSocketEventBridge;
4049
+ exports.bindWithRetry = bindWithRetry;
4001
4050
  exports.buildUpgradeResponse = buildUpgradeResponse;
4002
4051
  exports.computeAcceptKey = computeAcceptKey;
4003
4052
  exports.createDesignHandlers = createDesignHandlers;
@@ -4009,6 +4058,7 @@ exports.encodeCloseFrame = encodeCloseFrame;
4009
4058
  exports.encodePingFrame = encodePingFrame;
4010
4059
  exports.encodePongFrame = encodePongFrame;
4011
4060
  exports.encodeTextFrame = encodeTextFrame;
4061
+ exports.isAddrInUseError = isAddrInUseError;
4012
4062
  exports.isBatchRequest = isBatchRequest;
4013
4063
  exports.isSubscribe = isSubscribe;
4014
4064
  exports.isUnsubscribe = isUnsubscribe;