@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.
- package/dist/control/action-executor.d.ts.map +1 -1
- package/dist/control/index.js +2 -1
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +2 -1
- package/dist/control/index.mjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +60 -11
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +19 -10
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +19 -10
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/bind-retry.d.ts +64 -0
- package/dist/server/bind-retry.d.ts.map +1 -0
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +59 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +58 -10
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
|
1220
|
+
return success2({ state: resolveElementState(element) });
|
|
1214
1221
|
},
|
|
1215
1222
|
executeAction: async (ctx) => {
|
|
1216
|
-
const
|
|
1217
|
-
const
|
|
1218
|
-
|
|
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:
|
|
1223
|
-
params:
|
|
1224
|
-
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.
|
|
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;
|