@qontinui/ui-bridge-native 0.6.11 → 0.6.13
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/README.md +36 -16
- package/dist/control/action-executor.d.ts +42 -2
- package/dist/control/action-executor.d.ts.map +1 -1
- package/dist/control/index.js +723 -4
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +723 -5
- package/dist/control/index.mjs.map +1 -1
- package/dist/control/types.d.ts +82 -1
- package/dist/control/types.d.ts.map +1 -1
- package/dist/core/abortable.d.ts +151 -0
- package/dist/core/abortable.d.ts.map +1 -0
- package/dist/core/action-effect.d.ts +74 -0
- package/dist/core/action-effect.d.ts.map +1 -0
- package/dist/core/element-actions.d.ts +38 -0
- package/dist/core/element-actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +853 -15
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +835 -16
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/param-schema.d.ts +282 -0
- package/dist/core/param-schema.d.ts.map +1 -0
- package/dist/core/registry.d.ts +317 -2
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +231 -32
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1603 -118
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1585 -119
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +1 -23
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +1420 -113
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1420 -113
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIBridge.d.ts +19 -22
- package/dist/react/useUIBridge.d.ts.map +1 -1
- package/dist/react/useUIComponent.d.ts +113 -8
- package/dist/react/useUIComponent.d.ts.map +1 -1
- package/dist/react/useUIElement.d.ts +34 -8
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +15 -2
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +348 -43
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +348 -44
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/observability.d.ts +40 -0
- package/dist/server/observability.d.ts.map +1 -1
- package/dist/server/types.d.ts +57 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/transport/CloudRelayClient.d.ts.map +1 -1
- package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
- package/dist/transport/relay-logging.d.ts +50 -0
- package/dist/transport/relay-logging.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/core/index.js
CHANGED
|
@@ -1,5 +1,467 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/core/abortable.ts
|
|
4
|
+
var MAX_ACTION_TIMEOUT_MS = 864e5;
|
|
5
|
+
function normalizeActionTimeoutMs(raw) {
|
|
6
|
+
if (raw === void 0 || raw === null) {
|
|
7
|
+
return { ok: true, timeoutMs: void 0, clamped: false };
|
|
8
|
+
}
|
|
9
|
+
if (typeof raw !== "number") {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
reason: `timeoutMs must be a number of milliseconds, received ${typeof raw === "string" ? JSON.stringify(raw) : typeof raw}`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (!Number.isFinite(raw)) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: `timeoutMs must be a finite number of milliseconds, received ${String(raw)}`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (raw < 0) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
reason: `timeoutMs must not be negative, received ${String(raw)}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (raw > MAX_ACTION_TIMEOUT_MS) {
|
|
28
|
+
return { ok: true, timeoutMs: MAX_ACTION_TIMEOUT_MS, clamped: true };
|
|
29
|
+
}
|
|
30
|
+
return { ok: true, timeoutMs: Math.floor(raw), clamped: false };
|
|
31
|
+
}
|
|
32
|
+
function inertAbortSignal() {
|
|
33
|
+
return new AbortController().signal;
|
|
34
|
+
}
|
|
35
|
+
async function runAbortable(invoke, options = {}) {
|
|
36
|
+
const callerSignal = options.signal;
|
|
37
|
+
const timeoutMs = options.timeoutMs;
|
|
38
|
+
if (callerSignal?.aborted) {
|
|
39
|
+
return { aborted: true, reason: "signal" };
|
|
40
|
+
}
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
let reason = "signal";
|
|
43
|
+
let timer;
|
|
44
|
+
const onCallerAbort = () => {
|
|
45
|
+
reason = "signal";
|
|
46
|
+
controller.abort();
|
|
47
|
+
};
|
|
48
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
49
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0) {
|
|
50
|
+
timer = setTimeout(
|
|
51
|
+
() => {
|
|
52
|
+
reason = "timeout";
|
|
53
|
+
controller.abort();
|
|
54
|
+
},
|
|
55
|
+
Math.min(timeoutMs, MAX_ACTION_TIMEOUT_MS)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const abandoned = new Promise((resolve) => {
|
|
60
|
+
controller.signal.addEventListener("abort", () => resolve({ aborted: true, reason }), {
|
|
61
|
+
once: true
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
const completed = (async () => ({
|
|
65
|
+
aborted: false,
|
|
66
|
+
result: await invoke(controller.signal)
|
|
67
|
+
}))();
|
|
68
|
+
return await Promise.race([completed, abandoned]);
|
|
69
|
+
} finally {
|
|
70
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
71
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/core/param-schema.ts
|
|
76
|
+
var DEFAULT_PARAM_VALIDATION_MODE = "warn";
|
|
77
|
+
var currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
78
|
+
function setDefaultParamValidationMode(mode) {
|
|
79
|
+
currentDefaultMode = mode;
|
|
80
|
+
}
|
|
81
|
+
function getDefaultParamValidationMode() {
|
|
82
|
+
return currentDefaultMode;
|
|
83
|
+
}
|
|
84
|
+
function resetDefaultParamValidationMode() {
|
|
85
|
+
currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
86
|
+
}
|
|
87
|
+
var PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
88
|
+
"string",
|
|
89
|
+
"number",
|
|
90
|
+
"integer",
|
|
91
|
+
"boolean",
|
|
92
|
+
"object",
|
|
93
|
+
"array",
|
|
94
|
+
"null"
|
|
95
|
+
]);
|
|
96
|
+
var OBJECT_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
97
|
+
"type",
|
|
98
|
+
"properties",
|
|
99
|
+
"required",
|
|
100
|
+
"additionalProperties",
|
|
101
|
+
"patternProperties",
|
|
102
|
+
"propertyNames",
|
|
103
|
+
"minProperties",
|
|
104
|
+
"maxProperties",
|
|
105
|
+
"dependentRequired",
|
|
106
|
+
"dependentSchemas",
|
|
107
|
+
"unevaluatedProperties",
|
|
108
|
+
"allOf",
|
|
109
|
+
"anyOf",
|
|
110
|
+
"oneOf",
|
|
111
|
+
"not",
|
|
112
|
+
"if",
|
|
113
|
+
"then",
|
|
114
|
+
"else",
|
|
115
|
+
"enum",
|
|
116
|
+
"const",
|
|
117
|
+
"default",
|
|
118
|
+
"examples",
|
|
119
|
+
"title",
|
|
120
|
+
"description",
|
|
121
|
+
"deprecated",
|
|
122
|
+
"readOnly",
|
|
123
|
+
"writeOnly",
|
|
124
|
+
"$schema",
|
|
125
|
+
"$id",
|
|
126
|
+
"$ref",
|
|
127
|
+
"$comment",
|
|
128
|
+
"$defs",
|
|
129
|
+
"definitions"
|
|
130
|
+
]);
|
|
131
|
+
var MAX_SCHEMA_DEPTH = 32;
|
|
132
|
+
var MAX_PATTERN_LENGTH = 200;
|
|
133
|
+
function hasNestedQuantifiers(pattern) {
|
|
134
|
+
return /(\((?:[^()]*[+*}])[^()]*\))[+*?]|\(\?:[^()]*[+*}][^()]*\)[+*?]/.test(pattern);
|
|
135
|
+
}
|
|
136
|
+
function isPlainObject(value) {
|
|
137
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
138
|
+
}
|
|
139
|
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
140
|
+
function hasOwn(value, key) {
|
|
141
|
+
return hasOwnProperty.call(value, key);
|
|
142
|
+
}
|
|
143
|
+
function render(value) {
|
|
144
|
+
if (value === null) return "null";
|
|
145
|
+
if (value === void 0) return "undefined";
|
|
146
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
147
|
+
const t = typeof value;
|
|
148
|
+
if (t === "string") return JSON.stringify(value);
|
|
149
|
+
if (t === "number" || t === "boolean") return String(value);
|
|
150
|
+
if (t === "object") return "object";
|
|
151
|
+
return t;
|
|
152
|
+
}
|
|
153
|
+
var MAX_RENDERED_JSON_LENGTH = 120;
|
|
154
|
+
function renderJson(value) {
|
|
155
|
+
if (value === void 0) return "undefined";
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = JSON.stringify(value) ?? String(value);
|
|
159
|
+
} catch {
|
|
160
|
+
return render(value);
|
|
161
|
+
}
|
|
162
|
+
return text.length > MAX_RENDERED_JSON_LENGTH ? `${text.slice(0, MAX_RENDERED_JSON_LENGTH)}\u2026` : text;
|
|
163
|
+
}
|
|
164
|
+
function jsonTypeOf(value) {
|
|
165
|
+
if (value === null) return "null";
|
|
166
|
+
if (Array.isArray(value)) return "array";
|
|
167
|
+
const t = typeof value;
|
|
168
|
+
if (t === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
169
|
+
if (t === "string" || t === "boolean" || t === "object") return t;
|
|
170
|
+
return t;
|
|
171
|
+
}
|
|
172
|
+
function matchesType(value, typeName) {
|
|
173
|
+
switch (typeName) {
|
|
174
|
+
case "string":
|
|
175
|
+
return typeof value === "string";
|
|
176
|
+
case "number":
|
|
177
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
178
|
+
case "integer":
|
|
179
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
180
|
+
case "boolean":
|
|
181
|
+
return typeof value === "boolean";
|
|
182
|
+
case "null":
|
|
183
|
+
return value === null;
|
|
184
|
+
case "array":
|
|
185
|
+
return Array.isArray(value);
|
|
186
|
+
case "object":
|
|
187
|
+
return isPlainObject(value);
|
|
188
|
+
default:
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function jsonEquals(a, b, depth = 0) {
|
|
193
|
+
if (a === b) return true;
|
|
194
|
+
if (depth > MAX_SCHEMA_DEPTH) return false;
|
|
195
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
196
|
+
if (typeof a !== typeof b) return false;
|
|
197
|
+
if (typeof a !== "object") return false;
|
|
198
|
+
const aIsArray = Array.isArray(a);
|
|
199
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
200
|
+
if (aIsArray) {
|
|
201
|
+
const bArr = b;
|
|
202
|
+
const aArr = a;
|
|
203
|
+
if (aArr.length !== bArr.length) return false;
|
|
204
|
+
return aArr.every((item, i) => jsonEquals(item, bArr[i], depth + 1));
|
|
205
|
+
}
|
|
206
|
+
const aObj = a;
|
|
207
|
+
const bObj = b;
|
|
208
|
+
const aKeys = Object.keys(aObj);
|
|
209
|
+
const bKeys = Object.keys(bObj);
|
|
210
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
211
|
+
return aKeys.every((key) => hasOwn(bObj, key) && jsonEquals(aObj[key], bObj[key], depth + 1));
|
|
212
|
+
}
|
|
213
|
+
function joinPath(base, key) {
|
|
214
|
+
return base === "" ? key : `${base}.${key}`;
|
|
215
|
+
}
|
|
216
|
+
function readTypes(schema) {
|
|
217
|
+
const raw = schema["type"];
|
|
218
|
+
if (typeof raw === "string") return [raw];
|
|
219
|
+
if (Array.isArray(raw)) {
|
|
220
|
+
const names = raw.filter((t) => typeof t === "string");
|
|
221
|
+
return names.length > 0 ? names : null;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function validateValue(value, schema, path, issues, depth) {
|
|
226
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
227
|
+
if (typeof schema === "string") {
|
|
228
|
+
if (PRIMITIVE_TYPE_NAMES.has(schema) && !matchesType(value, schema)) {
|
|
229
|
+
issues.push({
|
|
230
|
+
path,
|
|
231
|
+
keyword: "type",
|
|
232
|
+
expected: schema,
|
|
233
|
+
received: render(value),
|
|
234
|
+
message: `Parameter "${path}" must be of type ${schema}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!isPlainObject(schema)) return;
|
|
240
|
+
const types = readTypes(schema);
|
|
241
|
+
if (types !== null && !types.some((t) => matchesType(value, t))) {
|
|
242
|
+
const expected = types.join(" | ");
|
|
243
|
+
issues.push({
|
|
244
|
+
path,
|
|
245
|
+
keyword: "type",
|
|
246
|
+
expected,
|
|
247
|
+
received: render(value),
|
|
248
|
+
message: `Parameter "${path}" must be of type ${expected}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (hasOwn(schema, "const") && !jsonEquals(value, schema["const"])) {
|
|
253
|
+
const expected = renderJson(schema["const"]);
|
|
254
|
+
const received = renderJson(value);
|
|
255
|
+
issues.push({
|
|
256
|
+
path,
|
|
257
|
+
keyword: "const",
|
|
258
|
+
expected,
|
|
259
|
+
received,
|
|
260
|
+
message: `Parameter "${path}" must equal ${expected}, received ${received}.`
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const enumValues = schema["enum"];
|
|
265
|
+
if (Array.isArray(enumValues) && !enumValues.some((allowed) => jsonEquals(value, allowed))) {
|
|
266
|
+
const expected = enumValues.map((v) => renderJson(v)).join(", ");
|
|
267
|
+
const received = renderJson(value);
|
|
268
|
+
issues.push({
|
|
269
|
+
path,
|
|
270
|
+
keyword: "enum",
|
|
271
|
+
expected,
|
|
272
|
+
received,
|
|
273
|
+
message: `Parameter "${path}" must be one of [${expected}], received ${received}.`
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (typeof value === "number") {
|
|
278
|
+
const min = schema["minimum"];
|
|
279
|
+
if (typeof min === "number" && value < min) {
|
|
280
|
+
issues.push({
|
|
281
|
+
path,
|
|
282
|
+
keyword: "minimum",
|
|
283
|
+
expected: `>= ${min}`,
|
|
284
|
+
received: render(value),
|
|
285
|
+
message: `Parameter "${path}" must be >= ${min}, received ${value}.`
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const max = schema["maximum"];
|
|
289
|
+
if (typeof max === "number" && value > max) {
|
|
290
|
+
issues.push({
|
|
291
|
+
path,
|
|
292
|
+
keyword: "maximum",
|
|
293
|
+
expected: `<= ${max}`,
|
|
294
|
+
received: render(value),
|
|
295
|
+
message: `Parameter "${path}" must be <= ${max}, received ${value}.`
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (typeof value === "string") {
|
|
300
|
+
const minLength = schema["minLength"];
|
|
301
|
+
if (typeof minLength === "number" && value.length < minLength) {
|
|
302
|
+
issues.push({
|
|
303
|
+
path,
|
|
304
|
+
keyword: "minLength",
|
|
305
|
+
expected: `length >= ${minLength}`,
|
|
306
|
+
received: `length ${value.length}`,
|
|
307
|
+
message: `Parameter "${path}" must be at least ${minLength} characters, received ${value.length}.`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const maxLength = schema["maxLength"];
|
|
311
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
312
|
+
issues.push({
|
|
313
|
+
path,
|
|
314
|
+
keyword: "maxLength",
|
|
315
|
+
expected: `length <= ${maxLength}`,
|
|
316
|
+
received: `length ${value.length}`,
|
|
317
|
+
message: `Parameter "${path}" must be at most ${maxLength} characters, received ${value.length}.`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const pattern = schema["pattern"];
|
|
321
|
+
if (typeof pattern === "string" && pattern.length <= MAX_PATTERN_LENGTH && !hasNestedQuantifiers(pattern)) {
|
|
322
|
+
let re;
|
|
323
|
+
try {
|
|
324
|
+
re = new RegExp(pattern);
|
|
325
|
+
} catch {
|
|
326
|
+
re = null;
|
|
327
|
+
}
|
|
328
|
+
if (re !== null && !re.test(value)) {
|
|
329
|
+
issues.push({
|
|
330
|
+
path,
|
|
331
|
+
keyword: "pattern",
|
|
332
|
+
expected: `match /${pattern}/`,
|
|
333
|
+
received: render(value),
|
|
334
|
+
message: `Parameter "${path}" must match /${pattern}/, received ${render(value)}.`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(value) && hasOwn(schema, "items")) {
|
|
340
|
+
const items = schema["items"];
|
|
341
|
+
value.forEach((element, index) => {
|
|
342
|
+
validateValue(element, items, `${path}[${index}]`, issues, depth + 1);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (isPlainObject(value) && (isPlainObject(schema["properties"]) || hasOwn(schema, "required") || schema["additionalProperties"] === false)) {
|
|
346
|
+
validateObject(value, schema, path, issues, depth);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function validateObject(value, schema, basePath, issues, depth) {
|
|
350
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
351
|
+
const properties = isPlainObject(schema["properties"]) ? schema["properties"] : {};
|
|
352
|
+
const requiredRaw = schema["required"];
|
|
353
|
+
const required = Array.isArray(requiredRaw) ? requiredRaw.filter((k) => typeof k === "string") : [];
|
|
354
|
+
for (const key of required) {
|
|
355
|
+
if (!hasOwn(value, key) || value[key] === void 0) {
|
|
356
|
+
const at = joinPath(basePath, key);
|
|
357
|
+
issues.push({
|
|
358
|
+
path: at,
|
|
359
|
+
keyword: "required",
|
|
360
|
+
expected: "present",
|
|
361
|
+
message: `Required parameter "${at}" is missing.`
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (schema["additionalProperties"] === false) {
|
|
366
|
+
const declared = new Set(Object.keys(properties));
|
|
367
|
+
const expected = declared.size === 0 ? "no parameters" : `one of [${[...declared].join(", ")}]`;
|
|
368
|
+
for (const key of Object.keys(value)) {
|
|
369
|
+
if (!declared.has(key)) {
|
|
370
|
+
const at = joinPath(basePath, key);
|
|
371
|
+
issues.push({
|
|
372
|
+
path: at,
|
|
373
|
+
keyword: "additionalProperties",
|
|
374
|
+
expected,
|
|
375
|
+
received: render(value[key]),
|
|
376
|
+
message: `Parameter "${at}" is not declared by the schema, which sets additionalProperties: false.`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
382
|
+
if (!hasOwn(value, key) || value[key] === void 0) continue;
|
|
383
|
+
validateValue(value[key], propSchema, joinPath(basePath, key), issues, depth + 1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function isObjectSchemaForm(schema) {
|
|
387
|
+
if (isPlainObject(schema["properties"])) return true;
|
|
388
|
+
if (schema["type"] !== "object") return false;
|
|
389
|
+
return Object.keys(schema).every((key) => OBJECT_SCHEMA_KEYWORDS.has(key));
|
|
390
|
+
}
|
|
391
|
+
function normalizeMapForm(schema) {
|
|
392
|
+
if (isObjectSchemaForm(schema)) return null;
|
|
393
|
+
const properties = {};
|
|
394
|
+
for (const [key, raw] of Object.entries(schema)) {
|
|
395
|
+
if (raw === null || raw === void 0) continue;
|
|
396
|
+
if (typeof raw === "string") {
|
|
397
|
+
properties[key] = PRIMITIVE_TYPE_NAMES.has(raw) ? { type: raw } : {};
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (isPlainObject(raw)) {
|
|
401
|
+
const { optional: _optional, ...rest } = raw;
|
|
402
|
+
properties[key] = rest;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { type: "object", properties };
|
|
407
|
+
}
|
|
408
|
+
function validateActionParams(schema, params) {
|
|
409
|
+
if (!isPlainObject(schema)) return { valid: true, issues: [] };
|
|
410
|
+
const normalized = normalizeMapForm(schema) ?? schema;
|
|
411
|
+
const hasProperties = isPlainObject(normalized["properties"]);
|
|
412
|
+
const hasRequired = Array.isArray(normalized["required"]);
|
|
413
|
+
const forbidsExtras = normalized["additionalProperties"] === false;
|
|
414
|
+
if (!hasProperties && !hasRequired && !forbidsExtras) {
|
|
415
|
+
return { valid: true, issues: [] };
|
|
416
|
+
}
|
|
417
|
+
const issues = [];
|
|
418
|
+
if (params === void 0 || params === null) {
|
|
419
|
+
validateObject({}, normalized, "", issues, 0);
|
|
420
|
+
return { valid: issues.length === 0, issues };
|
|
421
|
+
}
|
|
422
|
+
if (!isPlainObject(params)) {
|
|
423
|
+
issues.push({
|
|
424
|
+
path: "",
|
|
425
|
+
keyword: "type",
|
|
426
|
+
expected: "object",
|
|
427
|
+
received: render(params),
|
|
428
|
+
message: `Action params must be an object, received ${jsonTypeOf(params)} (${render(params)}).`
|
|
429
|
+
});
|
|
430
|
+
return { valid: false, issues };
|
|
431
|
+
}
|
|
432
|
+
validateObject(params, normalized, "", issues, 0);
|
|
433
|
+
return { valid: issues.length === 0, issues };
|
|
434
|
+
}
|
|
435
|
+
function formatParamValidationFailure(componentId, actionId, issues) {
|
|
436
|
+
const detail = issues.map((i) => i.message).join(" ");
|
|
437
|
+
return `Action "${actionId}" on component "${componentId}" was rejected: params do not match the action's declared paramSchema. ${detail}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/core/action-effect.ts
|
|
441
|
+
var NATIVE_STANDARD_ACTION_EFFECTS = {
|
|
442
|
+
// --- Reads ---------------------------------------------------------------
|
|
443
|
+
focus: "read",
|
|
444
|
+
blur: "read",
|
|
445
|
+
scroll: "read",
|
|
446
|
+
scrollIntoView: "read",
|
|
447
|
+
longPress: "read",
|
|
448
|
+
// --- Writes --------------------------------------------------------------
|
|
449
|
+
press: "write",
|
|
450
|
+
click: "write",
|
|
451
|
+
doubleTap: "write",
|
|
452
|
+
type: "write",
|
|
453
|
+
setValue: "write",
|
|
454
|
+
clear: "write",
|
|
455
|
+
swipe: "write",
|
|
456
|
+
toggle: "write"
|
|
457
|
+
};
|
|
458
|
+
function nativeStandardActionEffect(actionId) {
|
|
459
|
+
return Object.prototype.hasOwnProperty.call(NATIVE_STANDARD_ACTION_EFFECTS, actionId) ? NATIVE_STANDARD_ACTION_EFFECTS[actionId] : void 0;
|
|
460
|
+
}
|
|
461
|
+
function resolveActionEffect(action) {
|
|
462
|
+
return action.effect ?? nativeStandardActionEffect(action.id);
|
|
463
|
+
}
|
|
464
|
+
|
|
3
465
|
// src/core/vision-fields.ts
|
|
4
466
|
var INTERACTIVE_HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
5
467
|
"onPress",
|
|
@@ -173,7 +635,42 @@ function projectVisionFields(args) {
|
|
|
173
635
|
return fields;
|
|
174
636
|
}
|
|
175
637
|
|
|
638
|
+
// src/core/element-actions.ts
|
|
639
|
+
function serializeElementCustomActions(customActions) {
|
|
640
|
+
if (!customActions) return void 0;
|
|
641
|
+
const out = [];
|
|
642
|
+
for (const [id, action] of Object.entries(customActions)) {
|
|
643
|
+
if (!action) continue;
|
|
644
|
+
const entry = { id };
|
|
645
|
+
if (action.label !== void 0) entry.label = action.label;
|
|
646
|
+
if (action.description !== void 0) entry.description = action.description;
|
|
647
|
+
if (action.effect !== void 0) entry.effect = action.effect;
|
|
648
|
+
out.push(entry);
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
|
|
176
653
|
// src/core/registry.ts
|
|
654
|
+
function toRegisteredComponentActions(actions) {
|
|
655
|
+
return actions.map((a) => ({
|
|
656
|
+
id: a.id,
|
|
657
|
+
label: a.label,
|
|
658
|
+
description: a.description,
|
|
659
|
+
paramSchema: a.paramSchema,
|
|
660
|
+
effect: a.effect,
|
|
661
|
+
handler: a.handler
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
function publishedActionSignature(actions) {
|
|
665
|
+
return JSON.stringify(
|
|
666
|
+
actions.map((a) => [a.id, a.label, a.description, a.paramSchema, a.effect])
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
function sameElementIds(a, b) {
|
|
670
|
+
if (a === b) return true;
|
|
671
|
+
if (a === void 0 || b === void 0) return false;
|
|
672
|
+
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
673
|
+
}
|
|
177
674
|
function extractHandlerNames(props) {
|
|
178
675
|
if (!props) return [];
|
|
179
676
|
return Object.keys(props).filter((k) => typeof props[k] === "function");
|
|
@@ -182,6 +679,22 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
182
679
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
183
680
|
return registrationRoute === currentRoute;
|
|
184
681
|
}
|
|
682
|
+
function deriveActiveTabFromSegments(segments) {
|
|
683
|
+
if (!segments || segments.length === 0) return void 0;
|
|
684
|
+
let innermostGroup = -1;
|
|
685
|
+
for (let i = 0; i < segments.length; i++) {
|
|
686
|
+
const segment = segments[i];
|
|
687
|
+
if (typeof segment === "string" && segment.startsWith("(") && segment.endsWith(")")) {
|
|
688
|
+
innermostGroup = i;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (innermostGroup === -1) return void 0;
|
|
692
|
+
const next = segments[innermostGroup + 1];
|
|
693
|
+
if (next === void 0) return "index";
|
|
694
|
+
if (typeof next !== "string" || next.length === 0) return void 0;
|
|
695
|
+
if (next.startsWith("[") && next.endsWith("]")) return void 0;
|
|
696
|
+
return next;
|
|
697
|
+
}
|
|
185
698
|
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
186
699
|
if (visibility !== "visible") return void 0;
|
|
187
700
|
const layout = state.layout;
|
|
@@ -201,8 +714,58 @@ function projectBbox(state, visibility, pixelRatio = 1) {
|
|
|
201
714
|
h: toPx(layout.height)
|
|
202
715
|
};
|
|
203
716
|
}
|
|
204
|
-
function
|
|
205
|
-
const
|
|
717
|
+
function pageRectOf(state) {
|
|
718
|
+
const layout = state.layout;
|
|
719
|
+
if (!layout) return null;
|
|
720
|
+
if (layout.pageOriginUnmeasured === true) return null;
|
|
721
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
722
|
+
if (!hasPage) return null;
|
|
723
|
+
const left = layout.pageX;
|
|
724
|
+
const top = layout.pageY;
|
|
725
|
+
if (typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
return { left, top, right: left + layout.width, bottom: top + layout.height };
|
|
729
|
+
}
|
|
730
|
+
function spansOverlap(aMin, aMax, bMin, bMax) {
|
|
731
|
+
if (aMin === aMax) return aMin >= bMin && aMin <= bMax;
|
|
732
|
+
return aMin < bMax && aMax > bMin;
|
|
733
|
+
}
|
|
734
|
+
function isEmptyRect(rect) {
|
|
735
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
736
|
+
}
|
|
737
|
+
function intersectRects(a, b) {
|
|
738
|
+
if (!a) return b;
|
|
739
|
+
if (!b) return a;
|
|
740
|
+
return {
|
|
741
|
+
left: Math.max(a.left, b.left),
|
|
742
|
+
top: Math.max(a.top, b.top),
|
|
743
|
+
right: Math.min(a.right, b.right),
|
|
744
|
+
bottom: Math.min(a.bottom, b.bottom)
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function computeVisibility(state, clip) {
|
|
748
|
+
if (!state.visible) {
|
|
749
|
+
return { visibility: "hidden", visibilityReason: "hidden" };
|
|
750
|
+
}
|
|
751
|
+
if (state.layout === null) {
|
|
752
|
+
return { visibility: "likely-visible", visibilityReason: "no-layout" };
|
|
753
|
+
}
|
|
754
|
+
if (!clip) {
|
|
755
|
+
return { visibility: "visible" };
|
|
756
|
+
}
|
|
757
|
+
const rect = pageRectOf(state);
|
|
758
|
+
if (!rect) {
|
|
759
|
+
return { visibility: "visible" };
|
|
760
|
+
}
|
|
761
|
+
if (isEmptyRect(clip)) {
|
|
762
|
+
return { visibility: "hidden", visibilityReason: "off-screen" };
|
|
763
|
+
}
|
|
764
|
+
const onScreen = spansOverlap(rect.left, rect.right, clip.left, clip.right) && spansOverlap(rect.top, rect.bottom, clip.top, clip.bottom);
|
|
765
|
+
return onScreen ? { visibility: "visible" } : { visibility: "hidden", visibilityReason: "off-screen" };
|
|
766
|
+
}
|
|
767
|
+
function inferActions(type, scrollAncestorId) {
|
|
768
|
+
const baseActions = scrollAncestorId ? ["focus", "blur", "scrollIntoView"] : ["focus", "blur"];
|
|
206
769
|
switch (type) {
|
|
207
770
|
case "button":
|
|
208
771
|
case "touchable":
|
|
@@ -274,6 +837,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
274
837
|
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
275
838
|
*/
|
|
276
839
|
this.pixelRatio = 1;
|
|
840
|
+
/**
|
|
841
|
+
* Injected getter for the device window size in logical dp, used to clip
|
|
842
|
+
* reported visibility. Injected rather than read here for exactly the reason
|
|
843
|
+
* `pixelRatio` is: a static `import { Dimensions } from 'react-native'` in
|
|
844
|
+
* this non-React core module breaks the vitest suite and risks the
|
|
845
|
+
* Metro/Hermes `unknownModuleError`. It is a PROVIDER, not a value, so a
|
|
846
|
+
* rotation is picked up on the next snapshot with nothing to keep in sync.
|
|
847
|
+
*
|
|
848
|
+
* `null` means the window bounds are UNKNOWN, and {@link computeVisibility}
|
|
849
|
+
* treats unknown bounds as "cannot demote" — never as an empty viewport.
|
|
850
|
+
*/
|
|
851
|
+
this.viewportProvider = null;
|
|
277
852
|
/**
|
|
278
853
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
279
854
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -311,6 +886,30 @@ var NativeUIBridgeRegistry = class {
|
|
|
311
886
|
getRouteProvider() {
|
|
312
887
|
return this.routeProvider;
|
|
313
888
|
}
|
|
889
|
+
/**
|
|
890
|
+
* Resolve the snapshot's `activeTab`: an explicit `getActiveTab()` on the
|
|
891
|
+
* route provider wins, otherwise derive it from the Expo Router segments the
|
|
892
|
+
* provider already exposes.
|
|
893
|
+
*
|
|
894
|
+
* A throwing or empty-string provider degrades to the derivation rather than
|
|
895
|
+
* to `undefined` — the host's opt-in extra should never be able to make the
|
|
896
|
+
* snapshot WORSE than it was without it. A blank derivation stays blank.
|
|
897
|
+
*/
|
|
898
|
+
resolveActiveTab(segments) {
|
|
899
|
+
const explicit = (() => {
|
|
900
|
+
if (!this.routeProvider?.getActiveTab) return void 0;
|
|
901
|
+
try {
|
|
902
|
+
const value = this.routeProvider.getActiveTab();
|
|
903
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
904
|
+
} catch (error) {
|
|
905
|
+
if (this.config.verbose) {
|
|
906
|
+
console.warn(`[ui-bridge-native] routeProvider getActiveTab threw:`, error);
|
|
907
|
+
}
|
|
908
|
+
return void 0;
|
|
909
|
+
}
|
|
910
|
+
})();
|
|
911
|
+
return explicit ?? deriveActiveTabFromSegments(segments);
|
|
912
|
+
}
|
|
314
913
|
/**
|
|
315
914
|
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
316
915
|
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
@@ -326,6 +925,56 @@ var NativeUIBridgeRegistry = class {
|
|
|
326
925
|
getPixelRatio() {
|
|
327
926
|
return this.pixelRatio;
|
|
328
927
|
}
|
|
928
|
+
/**
|
|
929
|
+
* Set the getter for the device window size (logical dp) that reported
|
|
930
|
+
* visibility is clipped against. The React provider passes
|
|
931
|
+
* `() => Dimensions.get('window')` here at construction.
|
|
932
|
+
*
|
|
933
|
+
* Pass `null` to detach. While detached the window bounds are UNKNOWN and no
|
|
934
|
+
* element is ever demoted for being outside them.
|
|
935
|
+
*/
|
|
936
|
+
setViewportProvider(provider) {
|
|
937
|
+
this.viewportProvider = provider ?? null;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* The window as a page-space rect, or `null` when it is unknown or the
|
|
941
|
+
* provider returned something unusable (it throws in some RN teardown
|
|
942
|
+
* paths, and a zero-size window would silently hide the entire screen).
|
|
943
|
+
*/
|
|
944
|
+
getViewportRect() {
|
|
945
|
+
if (!this.viewportProvider) return null;
|
|
946
|
+
let size;
|
|
947
|
+
try {
|
|
948
|
+
size = this.viewportProvider();
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (this.config.verbose) {
|
|
951
|
+
console.warn(`[ui-bridge-native] viewportProvider threw:`, error);
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
const { width, height } = size ?? {};
|
|
956
|
+
if (typeof width !== "number" || typeof height !== "number" || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
return { left: 0, top: 0, right: width, bottom: height };
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* The region `element` must reach to count as on screen: the window,
|
|
963
|
+
* intersected with the measured frame of its declared `scrollAncestorId`.
|
|
964
|
+
*
|
|
965
|
+
* Returns `null` when neither bound is known — see {@link computeVisibility}
|
|
966
|
+
* for why that means "do not demote" rather than "nothing is visible".
|
|
967
|
+
* An ancestor that is not registered, or registered but not yet measured,
|
|
968
|
+
* contributes nothing rather than hiding its subtree.
|
|
969
|
+
*/
|
|
970
|
+
getClipRectFor(element, viewport) {
|
|
971
|
+
const bounds = viewport === void 0 ? this.getViewportRect() : viewport;
|
|
972
|
+
const ancestorId = element.scrollAncestorId;
|
|
973
|
+
if (!ancestorId) return bounds;
|
|
974
|
+
const ancestor = this.elements.get(ancestorId);
|
|
975
|
+
if (!ancestor || ancestorId === element.id) return bounds;
|
|
976
|
+
return intersectRects(bounds, pageRectOf(ancestor.getState()));
|
|
977
|
+
}
|
|
329
978
|
// ============================================================================
|
|
330
979
|
// Snapshot Enricher Slots
|
|
331
980
|
// ============================================================================
|
|
@@ -369,16 +1018,17 @@ var NativeUIBridgeRegistry = class {
|
|
|
369
1018
|
const {
|
|
370
1019
|
type = "custom",
|
|
371
1020
|
label,
|
|
372
|
-
actions = inferActions(type),
|
|
373
1021
|
customActions,
|
|
374
1022
|
props,
|
|
375
1023
|
treePath = id,
|
|
376
1024
|
testId,
|
|
377
1025
|
accessibilityLabel,
|
|
378
1026
|
registrationRoute,
|
|
1027
|
+
scrollAncestorId,
|
|
379
1028
|
flatStyle,
|
|
380
1029
|
stateStyles
|
|
381
1030
|
} = options;
|
|
1031
|
+
const actions = options.actions ?? inferActions(type, scrollAncestorId);
|
|
382
1032
|
const getState = () => {
|
|
383
1033
|
const element = ref.current;
|
|
384
1034
|
if (!element) {
|
|
@@ -421,6 +1071,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
421
1071
|
registeredAt: Date.now(),
|
|
422
1072
|
mounted: true,
|
|
423
1073
|
registrationRoute: registrationRoute ?? null,
|
|
1074
|
+
scrollAncestorId,
|
|
424
1075
|
flatStyle,
|
|
425
1076
|
stateStyles
|
|
426
1077
|
};
|
|
@@ -551,6 +1202,103 @@ var NativeUIBridgeRegistry = class {
|
|
|
551
1202
|
console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
|
|
552
1203
|
}
|
|
553
1204
|
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Re-measure every mounted-visible element's geometry via its stored React
|
|
1207
|
+
* ref and write the fresh coordinates into the registry ("measure-on-
|
|
1208
|
+
* snapshot").
|
|
1209
|
+
*
|
|
1210
|
+
* Why: element geometry is otherwise captured ONCE at mount time
|
|
1211
|
+
* (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
|
|
1212
|
+
* `onLayout` on pure translation — scrolling a virtualized list
|
|
1213
|
+
* (FlashList), recycling a cell, a keyboard shift or an orientation change
|
|
1214
|
+
* all move elements WITHOUT updating the stored `state.layout`. Snapshot
|
|
1215
|
+
* consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
|
|
1216
|
+
* operate on stale positions. Re-measuring at READ time is correct
|
|
1217
|
+
* regardless of what moved the element and needs zero per-consumer wiring:
|
|
1218
|
+
* every registry entry already stores its ref (see `registerElement`).
|
|
1219
|
+
*
|
|
1220
|
+
* Per-element outcomes:
|
|
1221
|
+
* - non-zero dims → write a fresh full layout (same state shape as the
|
|
1222
|
+
* mount-time measure loop in `useUIElement`); counted as `measured`.
|
|
1223
|
+
* - zero dims AND the element previously had a non-null `state.layout` →
|
|
1224
|
+
* the view collapsed / left the screen; clear to
|
|
1225
|
+
* `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
|
|
1226
|
+
* writes); counted as `cleared`.
|
|
1227
|
+
* - zero dims and it never had a layout → leave state untouched (element
|
|
1228
|
+
* in the mount→first-onLayout gap; don't demote `likely-visible`);
|
|
1229
|
+
* counted as `skipped`.
|
|
1230
|
+
* - no ref / no callable `measureInWindow` (test fixtures, web) → leave
|
|
1231
|
+
* untouched; counted as `skipped`.
|
|
1232
|
+
*
|
|
1233
|
+
* The whole sweep races an overall timeout (default 250 ms) so a dead ref
|
|
1234
|
+
* whose `measureInWindow` never calls back can never hang a snapshot: the
|
|
1235
|
+
* returned promise resolves at the deadline with whatever counts have
|
|
1236
|
+
* accumulated. Late callbacks may still write state afterwards — harmless,
|
|
1237
|
+
* freshest data wins. Never throws.
|
|
1238
|
+
*
|
|
1239
|
+
* Non-React core module on purpose — `measureInWindow` is duck-typed off
|
|
1240
|
+
* the stored ref rather than imported from `react-native` (see the
|
|
1241
|
+
* `projectBbox` doc comment for why this file must not import RN).
|
|
1242
|
+
*/
|
|
1243
|
+
async refreshMeasurements(options) {
|
|
1244
|
+
const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
|
|
1245
|
+
const counts = { measured: 0, cleared: 0, skipped: 0 };
|
|
1246
|
+
const pending = [];
|
|
1247
|
+
for (const element of this.getMountedVisibleElements()) {
|
|
1248
|
+
const node = element.ref?.current;
|
|
1249
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
1250
|
+
counts.skipped++;
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const id = element.id;
|
|
1254
|
+
pending.push(
|
|
1255
|
+
new Promise((resolve) => {
|
|
1256
|
+
let settled = false;
|
|
1257
|
+
try {
|
|
1258
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
1259
|
+
if (settled) return;
|
|
1260
|
+
settled = true;
|
|
1261
|
+
try {
|
|
1262
|
+
if (w > 0 && h > 0) {
|
|
1263
|
+
this.updateElementState(id, {
|
|
1264
|
+
mounted: true,
|
|
1265
|
+
visible: true,
|
|
1266
|
+
enabled: true,
|
|
1267
|
+
focused: false,
|
|
1268
|
+
layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
|
|
1269
|
+
});
|
|
1270
|
+
counts.measured++;
|
|
1271
|
+
} else if (this.elements.get(id)?.getState().layout != null) {
|
|
1272
|
+
this.updateElementState(id, { visible: false, layout: null });
|
|
1273
|
+
counts.cleared++;
|
|
1274
|
+
} else {
|
|
1275
|
+
counts.skipped++;
|
|
1276
|
+
}
|
|
1277
|
+
} finally {
|
|
1278
|
+
resolve();
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
} catch {
|
|
1282
|
+
if (!settled) {
|
|
1283
|
+
settled = true;
|
|
1284
|
+
counts.skipped++;
|
|
1285
|
+
resolve();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
})
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
if (pending.length > 0) {
|
|
1292
|
+
await new Promise((resolve) => {
|
|
1293
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
1294
|
+
void Promise.all(pending).then(() => {
|
|
1295
|
+
clearTimeout(timer);
|
|
1296
|
+
resolve();
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
return counts;
|
|
1301
|
+
}
|
|
554
1302
|
/**
|
|
555
1303
|
* Update element state
|
|
556
1304
|
*/
|
|
@@ -700,12 +1448,8 @@ var NativeUIBridgeRegistry = class {
|
|
|
700
1448
|
id,
|
|
701
1449
|
name,
|
|
702
1450
|
description,
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
label: a.label,
|
|
706
|
-
description: a.description,
|
|
707
|
-
handler: a.handler
|
|
708
|
-
})),
|
|
1451
|
+
// Closed field list, applied once — see `toRegisteredComponentActions`.
|
|
1452
|
+
actions: toRegisteredComponentActions(actions),
|
|
709
1453
|
elementIds,
|
|
710
1454
|
registeredAt: Date.now(),
|
|
711
1455
|
mounted: true
|
|
@@ -717,6 +1461,55 @@ var NativeUIBridgeRegistry = class {
|
|
|
717
1461
|
}
|
|
718
1462
|
return registered;
|
|
719
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Update a registered component's declaration in place — name, description,
|
|
1466
|
+
* actions, elementIds.
|
|
1467
|
+
*
|
|
1468
|
+
* Sister to {@link updateElementMeta}, and it exists for the same reason one
|
|
1469
|
+
* level up. `registerComponent` publishes the declaration exactly once, and
|
|
1470
|
+
* `useUIComponent` registers on mount and never again, so an action whose
|
|
1471
|
+
* `label` carries a count, an action that only exists while a row is
|
|
1472
|
+
* selected, a `paramSchema` that widens once options load, or an `elementIds`
|
|
1473
|
+
* list that grows after mount, all published their mount-time shape to
|
|
1474
|
+
* `/control/components` forever. PR #176 closed that hole for an ELEMENT's
|
|
1475
|
+
* label; this closes it for a COMPONENT's declaration.
|
|
1476
|
+
*
|
|
1477
|
+
* **Idempotent by VALUE over the PUBLISHED shape.** `handler` identity is
|
|
1478
|
+
* excluded from the comparison — call sites pass inline closures that are
|
|
1479
|
+
* freshly allocated every render, so including it would report a change on
|
|
1480
|
+
* every call and make the door useless for a render-path publisher. The
|
|
1481
|
+
* newest handlers are still STORED whenever `actions` is supplied, which is
|
|
1482
|
+
* the other half of the same defect: without it the registry keeps the
|
|
1483
|
+
* closure from the render that registered the component, so a handler reading
|
|
1484
|
+
* component state read the state as it was at mount.
|
|
1485
|
+
*
|
|
1486
|
+
* Returns `true` when the published shape changed and an event was emitted,
|
|
1487
|
+
* `false` otherwise — the same contract as `updateElementMeta`, so a caller
|
|
1488
|
+
* can gate a warning or a state write on a real change.
|
|
1489
|
+
*/
|
|
1490
|
+
updateComponentMeta(id, meta) {
|
|
1491
|
+
const component = this.components.get(id);
|
|
1492
|
+
if (!component) return false;
|
|
1493
|
+
const nextName = meta.name !== void 0 ? meta.name : component.name;
|
|
1494
|
+
const nextDescription = meta.description !== void 0 ? meta.description : component.description;
|
|
1495
|
+
const nextActions = meta.actions !== void 0 ? toRegisteredComponentActions(meta.actions) : component.actions;
|
|
1496
|
+
const nextElementIds = meta.elementIds !== void 0 ? meta.elementIds : component.elementIds;
|
|
1497
|
+
const changed = nextName !== component.name || nextDescription !== component.description || !sameElementIds(nextElementIds, component.elementIds) || publishedActionSignature(nextActions) !== publishedActionSignature(component.actions);
|
|
1498
|
+
if (!changed && meta.actions === void 0) return false;
|
|
1499
|
+
this.components.set(id, {
|
|
1500
|
+
...component,
|
|
1501
|
+
name: nextName,
|
|
1502
|
+
description: nextDescription,
|
|
1503
|
+
actions: nextActions,
|
|
1504
|
+
elementIds: nextElementIds
|
|
1505
|
+
});
|
|
1506
|
+
if (!changed) return false;
|
|
1507
|
+
this.emit("component:registered", { id, name: nextName });
|
|
1508
|
+
if (this.config.verbose) {
|
|
1509
|
+
console.log(`[ui-bridge-native] Updated component: ${id} (${nextName})`);
|
|
1510
|
+
}
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
720
1513
|
/**
|
|
721
1514
|
* Unregister a component
|
|
722
1515
|
*/
|
|
@@ -830,41 +1623,59 @@ var NativeUIBridgeRegistry = class {
|
|
|
830
1623
|
* server-level override (the bare-server / cloud-relay path) still get a
|
|
831
1624
|
* populated `currentRoute` field as long as `setRouteProvider` was called
|
|
832
1625
|
* on the registry.
|
|
1626
|
+
*
|
|
1627
|
+
* `activeTab` resolves through {@link resolveActiveTab} on BOTH branches. An
|
|
1628
|
+
* explicit `routeInfo.activeTab` wins outright; otherwise a registered
|
|
1629
|
+
* provider's `getActiveTab()` answers and the Expo Router derivation is the
|
|
1630
|
+
* fallback. Deriving straight from `routeInfo.segments` here instead — which
|
|
1631
|
+
* is what this branch did when `activeTab` was introduced — silently ignored
|
|
1632
|
+
* `RouteProvider.getActiveTab` on the ONLY path that serves
|
|
1633
|
+
* `control/snapshot` once `setRouteProvider` is wired (the server installs a
|
|
1634
|
+
* `getSnapshot` override that passes `routeInfo`), so the opt-in hook for an
|
|
1635
|
+
* app whose visible pane is decoupled from the router reached no consumer.
|
|
833
1636
|
*/
|
|
834
1637
|
createSnapshot(routeInfo, options) {
|
|
835
1638
|
const resolvedRoute = (() => {
|
|
836
1639
|
if (routeInfo !== void 0) {
|
|
1640
|
+
const explicit = typeof routeInfo.activeTab === "string" && routeInfo.activeTab.length > 0 ? routeInfo.activeTab : void 0;
|
|
837
1641
|
return {
|
|
838
1642
|
currentRoute: routeInfo.currentRoute ?? null,
|
|
839
|
-
segments: routeInfo.segments
|
|
1643
|
+
segments: routeInfo.segments,
|
|
1644
|
+
activeTab: explicit ?? this.resolveActiveTab(routeInfo.segments)
|
|
840
1645
|
};
|
|
841
1646
|
}
|
|
842
1647
|
if (this.routeProvider) {
|
|
843
1648
|
try {
|
|
1649
|
+
const segments = this.routeProvider.getSegments?.();
|
|
844
1650
|
return {
|
|
845
1651
|
currentRoute: this.routeProvider.getCurrentRoute() ?? null,
|
|
846
|
-
segments
|
|
1652
|
+
segments,
|
|
1653
|
+
activeTab: this.resolveActiveTab(segments)
|
|
847
1654
|
};
|
|
848
1655
|
} catch (error) {
|
|
849
1656
|
if (this.config.verbose) {
|
|
850
1657
|
console.warn(`[ui-bridge-native] routeProvider getCurrentRoute threw:`, error);
|
|
851
1658
|
}
|
|
852
|
-
return { currentRoute: null, segments: void 0 };
|
|
1659
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
853
1660
|
}
|
|
854
1661
|
}
|
|
855
|
-
return { currentRoute: null, segments: void 0 };
|
|
1662
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
856
1663
|
})();
|
|
857
1664
|
let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
|
|
858
1665
|
if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
|
|
859
1666
|
const currentRoute = resolvedRoute.currentRoute;
|
|
860
1667
|
elements = elements.filter((e) => matchesCurrentRoute(e.registrationRoute, currentRoute));
|
|
861
1668
|
}
|
|
1669
|
+
const viewport = this.getViewportRect();
|
|
862
1670
|
const snapshot = {
|
|
863
1671
|
timestamp: Date.now(),
|
|
864
1672
|
elements: elements.map((e) => {
|
|
865
1673
|
const handlers = extractHandlerNames(e.props);
|
|
866
1674
|
const state = e.getState();
|
|
867
|
-
const visibility
|
|
1675
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
1676
|
+
state,
|
|
1677
|
+
this.getClipRectFor(e, viewport)
|
|
1678
|
+
);
|
|
868
1679
|
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
869
1680
|
const vision = projectVisionFields({
|
|
870
1681
|
type: e.type,
|
|
@@ -884,10 +1695,11 @@ var NativeUIBridgeRegistry = class {
|
|
|
884
1695
|
identifier: e.getIdentifier(),
|
|
885
1696
|
state,
|
|
886
1697
|
actions: e.actions,
|
|
887
|
-
customActions:
|
|
1698
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
888
1699
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
889
1700
|
registrationRoute: e.registrationRoute,
|
|
890
1701
|
visibility,
|
|
1702
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
891
1703
|
...bbox ? { bbox } : {},
|
|
892
1704
|
...vision
|
|
893
1705
|
};
|
|
@@ -907,6 +1719,13 @@ var NativeUIBridgeRegistry = class {
|
|
|
907
1719
|
})),
|
|
908
1720
|
currentRoute: resolvedRoute.currentRoute,
|
|
909
1721
|
segments: resolvedRoute.segments,
|
|
1722
|
+
// Canonical aliases, spelled the way the web SDK's `BridgeSnapshot`
|
|
1723
|
+
// spells them so a cross-platform consumer reads one field name on both
|
|
1724
|
+
// platforms. Omitted rather than nulled when unknown — same convention
|
|
1725
|
+
// as the web fields, and it keeps the emitted shape byte-identical for
|
|
1726
|
+
// hosts that wire no route provider.
|
|
1727
|
+
...resolvedRoute.currentRoute ? { route: resolvedRoute.currentRoute } : {},
|
|
1728
|
+
...resolvedRoute.activeTab ? { activeTab: resolvedRoute.activeTab } : {},
|
|
910
1729
|
registration: this.getRegistrationCoverage()
|
|
911
1730
|
};
|
|
912
1731
|
if (this.appInfo) {
|
|
@@ -1104,18 +1923,37 @@ function matchesIdentifier(identifier, criteria) {
|
|
|
1104
1923
|
return true;
|
|
1105
1924
|
}
|
|
1106
1925
|
|
|
1926
|
+
exports.DEFAULT_PARAM_VALIDATION_MODE = DEFAULT_PARAM_VALIDATION_MODE;
|
|
1927
|
+
exports.MAX_ACTION_TIMEOUT_MS = MAX_ACTION_TIMEOUT_MS;
|
|
1928
|
+
exports.NATIVE_STANDARD_ACTION_EFFECTS = NATIVE_STANDARD_ACTION_EFFECTS;
|
|
1107
1929
|
exports.NativeUIBridgeRegistry = NativeUIBridgeRegistry;
|
|
1108
1930
|
exports.buildTreePath = buildTreePath;
|
|
1931
|
+
exports.computeVisibility = computeVisibility;
|
|
1109
1932
|
exports.createNativeElementIdentifier = createNativeElementIdentifier;
|
|
1933
|
+
exports.deriveActiveTabFromSegments = deriveActiveTabFromSegments;
|
|
1110
1934
|
exports.extractHandlerNames = extractHandlerNames;
|
|
1111
1935
|
exports.findAllByPattern = findAllByPattern;
|
|
1112
1936
|
exports.findElementByIdentifier = findElementByIdentifier;
|
|
1937
|
+
exports.formatParamValidationFailure = formatParamValidationFailure;
|
|
1938
|
+
exports.getDefaultParamValidationMode = getDefaultParamValidationMode;
|
|
1113
1939
|
exports.getGlobalRegistry = getGlobalRegistry;
|
|
1940
|
+
exports.inertAbortSignal = inertAbortSignal;
|
|
1941
|
+
exports.intersectRects = intersectRects;
|
|
1942
|
+
exports.isEmptyRect = isEmptyRect;
|
|
1114
1943
|
exports.matchesCurrentRoute = matchesCurrentRoute;
|
|
1115
1944
|
exports.matchesIdentifier = matchesIdentifier;
|
|
1945
|
+
exports.nativeStandardActionEffect = nativeStandardActionEffect;
|
|
1946
|
+
exports.normalizeActionTimeoutMs = normalizeActionTimeoutMs;
|
|
1947
|
+
exports.pageRectOf = pageRectOf;
|
|
1116
1948
|
exports.parseTreePath = parseTreePath;
|
|
1117
1949
|
exports.projectBbox = projectBbox;
|
|
1950
|
+
exports.resetDefaultParamValidationMode = resetDefaultParamValidationMode;
|
|
1118
1951
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
1952
|
+
exports.resolveActionEffect = resolveActionEffect;
|
|
1953
|
+
exports.runAbortable = runAbortable;
|
|
1954
|
+
exports.serializeElementCustomActions = serializeElementCustomActions;
|
|
1955
|
+
exports.setDefaultParamValidationMode = setDefaultParamValidationMode;
|
|
1119
1956
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
1957
|
+
exports.validateActionParams = validateActionParams;
|
|
1120
1958
|
//# sourceMappingURL=index.js.map
|
|
1121
1959
|
//# sourceMappingURL=index.js.map
|