@qontinui/ui-bridge-native 0.6.12 → 0.6.14
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 +756 -15
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +738 -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 +272 -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 +1568 -135
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1550 -136
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +18 -24
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +1385 -130
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1385 -130
- 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 +345 -43
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +345 -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 +35 -7
- package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.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/control/index.js
CHANGED
|
@@ -1,12 +1,469 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// src/core/registry.ts
|
|
4
|
+
function pageRectOf(state) {
|
|
5
|
+
const layout = state.layout;
|
|
6
|
+
if (!layout) return null;
|
|
7
|
+
if (layout.pageOriginUnmeasured === true) return null;
|
|
8
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
9
|
+
if (!hasPage) return null;
|
|
10
|
+
const left = layout.pageX;
|
|
11
|
+
const top = layout.pageY;
|
|
12
|
+
if (typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return { left, top, right: left + layout.width, bottom: top + layout.height };
|
|
16
|
+
}
|
|
17
|
+
function isEmptyRect(rect) {
|
|
18
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
19
|
+
}
|
|
20
|
+
function intersectRects(a, b) {
|
|
21
|
+
if (!a) return b;
|
|
22
|
+
if (!b) return a;
|
|
23
|
+
return {
|
|
24
|
+
left: Math.max(a.left, b.left),
|
|
25
|
+
top: Math.max(a.top, b.top),
|
|
26
|
+
right: Math.min(a.right, b.right),
|
|
27
|
+
bottom: Math.min(a.bottom, b.bottom)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
// src/core/element-identifier.ts
|
|
6
32
|
function findElementByIdentifier(identifier) {
|
|
7
33
|
return null;
|
|
8
34
|
}
|
|
9
35
|
|
|
36
|
+
// src/core/abortable.ts
|
|
37
|
+
var MAX_ACTION_TIMEOUT_MS = 864e5;
|
|
38
|
+
function normalizeActionTimeoutMs(raw) {
|
|
39
|
+
if (raw === void 0 || raw === null) {
|
|
40
|
+
return { ok: true, timeoutMs: void 0, clamped: false };
|
|
41
|
+
}
|
|
42
|
+
if (typeof raw !== "number") {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
reason: `timeoutMs must be a number of milliseconds, received ${typeof raw === "string" ? JSON.stringify(raw) : typeof raw}`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isFinite(raw)) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
reason: `timeoutMs must be a finite number of milliseconds, received ${String(raw)}`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (raw < 0) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: `timeoutMs must not be negative, received ${String(raw)}`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (raw > MAX_ACTION_TIMEOUT_MS) {
|
|
61
|
+
return { ok: true, timeoutMs: MAX_ACTION_TIMEOUT_MS, clamped: true };
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, timeoutMs: Math.floor(raw), clamped: false };
|
|
64
|
+
}
|
|
65
|
+
function inertAbortSignal() {
|
|
66
|
+
return new AbortController().signal;
|
|
67
|
+
}
|
|
68
|
+
async function runAbortable(invoke, options = {}) {
|
|
69
|
+
const callerSignal = options.signal;
|
|
70
|
+
const timeoutMs = options.timeoutMs;
|
|
71
|
+
if (callerSignal?.aborted) {
|
|
72
|
+
return { aborted: true, reason: "signal" };
|
|
73
|
+
}
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
let reason = "signal";
|
|
76
|
+
let timer;
|
|
77
|
+
const onCallerAbort = () => {
|
|
78
|
+
reason = "signal";
|
|
79
|
+
controller.abort();
|
|
80
|
+
};
|
|
81
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
82
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0) {
|
|
83
|
+
timer = setTimeout(
|
|
84
|
+
() => {
|
|
85
|
+
reason = "timeout";
|
|
86
|
+
controller.abort();
|
|
87
|
+
},
|
|
88
|
+
Math.min(timeoutMs, MAX_ACTION_TIMEOUT_MS)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const abandoned = new Promise((resolve) => {
|
|
93
|
+
controller.signal.addEventListener("abort", () => resolve({ aborted: true, reason }), {
|
|
94
|
+
once: true
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
const completed = (async () => ({
|
|
98
|
+
aborted: false,
|
|
99
|
+
result: await invoke(controller.signal)
|
|
100
|
+
}))();
|
|
101
|
+
return await Promise.race([completed, abandoned]);
|
|
102
|
+
} finally {
|
|
103
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
104
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/core/param-schema.ts
|
|
109
|
+
var DEFAULT_PARAM_VALIDATION_MODE = "warn";
|
|
110
|
+
var currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
111
|
+
function getDefaultParamValidationMode() {
|
|
112
|
+
return currentDefaultMode;
|
|
113
|
+
}
|
|
114
|
+
var PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
115
|
+
"string",
|
|
116
|
+
"number",
|
|
117
|
+
"integer",
|
|
118
|
+
"boolean",
|
|
119
|
+
"object",
|
|
120
|
+
"array",
|
|
121
|
+
"null"
|
|
122
|
+
]);
|
|
123
|
+
var OBJECT_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
124
|
+
"type",
|
|
125
|
+
"properties",
|
|
126
|
+
"required",
|
|
127
|
+
"additionalProperties",
|
|
128
|
+
"patternProperties",
|
|
129
|
+
"propertyNames",
|
|
130
|
+
"minProperties",
|
|
131
|
+
"maxProperties",
|
|
132
|
+
"dependentRequired",
|
|
133
|
+
"dependentSchemas",
|
|
134
|
+
"unevaluatedProperties",
|
|
135
|
+
"allOf",
|
|
136
|
+
"anyOf",
|
|
137
|
+
"oneOf",
|
|
138
|
+
"not",
|
|
139
|
+
"if",
|
|
140
|
+
"then",
|
|
141
|
+
"else",
|
|
142
|
+
"enum",
|
|
143
|
+
"const",
|
|
144
|
+
"default",
|
|
145
|
+
"examples",
|
|
146
|
+
"title",
|
|
147
|
+
"description",
|
|
148
|
+
"deprecated",
|
|
149
|
+
"readOnly",
|
|
150
|
+
"writeOnly",
|
|
151
|
+
"$schema",
|
|
152
|
+
"$id",
|
|
153
|
+
"$ref",
|
|
154
|
+
"$comment",
|
|
155
|
+
"$defs",
|
|
156
|
+
"definitions"
|
|
157
|
+
]);
|
|
158
|
+
var MAX_SCHEMA_DEPTH = 32;
|
|
159
|
+
var MAX_PATTERN_LENGTH = 200;
|
|
160
|
+
function hasNestedQuantifiers(pattern) {
|
|
161
|
+
return /(\((?:[^()]*[+*}])[^()]*\))[+*?]|\(\?:[^()]*[+*}][^()]*\)[+*?]/.test(pattern);
|
|
162
|
+
}
|
|
163
|
+
function isPlainObject(value) {
|
|
164
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
165
|
+
}
|
|
166
|
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
167
|
+
function hasOwn(value, key) {
|
|
168
|
+
return hasOwnProperty.call(value, key);
|
|
169
|
+
}
|
|
170
|
+
function render(value) {
|
|
171
|
+
if (value === null) return "null";
|
|
172
|
+
if (value === void 0) return "undefined";
|
|
173
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
174
|
+
const t = typeof value;
|
|
175
|
+
if (t === "string") return JSON.stringify(value);
|
|
176
|
+
if (t === "number" || t === "boolean") return String(value);
|
|
177
|
+
if (t === "object") return "object";
|
|
178
|
+
return t;
|
|
179
|
+
}
|
|
180
|
+
var MAX_RENDERED_JSON_LENGTH = 120;
|
|
181
|
+
function renderJson(value) {
|
|
182
|
+
if (value === void 0) return "undefined";
|
|
183
|
+
let text;
|
|
184
|
+
try {
|
|
185
|
+
text = JSON.stringify(value) ?? String(value);
|
|
186
|
+
} catch {
|
|
187
|
+
return render(value);
|
|
188
|
+
}
|
|
189
|
+
return text.length > MAX_RENDERED_JSON_LENGTH ? `${text.slice(0, MAX_RENDERED_JSON_LENGTH)}\u2026` : text;
|
|
190
|
+
}
|
|
191
|
+
function jsonTypeOf(value) {
|
|
192
|
+
if (value === null) return "null";
|
|
193
|
+
if (Array.isArray(value)) return "array";
|
|
194
|
+
const t = typeof value;
|
|
195
|
+
if (t === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
196
|
+
if (t === "string" || t === "boolean" || t === "object") return t;
|
|
197
|
+
return t;
|
|
198
|
+
}
|
|
199
|
+
function matchesType(value, typeName) {
|
|
200
|
+
switch (typeName) {
|
|
201
|
+
case "string":
|
|
202
|
+
return typeof value === "string";
|
|
203
|
+
case "number":
|
|
204
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
205
|
+
case "integer":
|
|
206
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
207
|
+
case "boolean":
|
|
208
|
+
return typeof value === "boolean";
|
|
209
|
+
case "null":
|
|
210
|
+
return value === null;
|
|
211
|
+
case "array":
|
|
212
|
+
return Array.isArray(value);
|
|
213
|
+
case "object":
|
|
214
|
+
return isPlainObject(value);
|
|
215
|
+
default:
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function jsonEquals(a, b, depth = 0) {
|
|
220
|
+
if (a === b) return true;
|
|
221
|
+
if (depth > MAX_SCHEMA_DEPTH) return false;
|
|
222
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
223
|
+
if (typeof a !== typeof b) return false;
|
|
224
|
+
if (typeof a !== "object") return false;
|
|
225
|
+
const aIsArray = Array.isArray(a);
|
|
226
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
227
|
+
if (aIsArray) {
|
|
228
|
+
const bArr = b;
|
|
229
|
+
const aArr = a;
|
|
230
|
+
if (aArr.length !== bArr.length) return false;
|
|
231
|
+
return aArr.every((item, i) => jsonEquals(item, bArr[i], depth + 1));
|
|
232
|
+
}
|
|
233
|
+
const aObj = a;
|
|
234
|
+
const bObj = b;
|
|
235
|
+
const aKeys = Object.keys(aObj);
|
|
236
|
+
const bKeys = Object.keys(bObj);
|
|
237
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
238
|
+
return aKeys.every((key) => hasOwn(bObj, key) && jsonEquals(aObj[key], bObj[key], depth + 1));
|
|
239
|
+
}
|
|
240
|
+
function joinPath(base, key) {
|
|
241
|
+
return base === "" ? key : `${base}.${key}`;
|
|
242
|
+
}
|
|
243
|
+
function readTypes(schema) {
|
|
244
|
+
const raw = schema["type"];
|
|
245
|
+
if (typeof raw === "string") return [raw];
|
|
246
|
+
if (Array.isArray(raw)) {
|
|
247
|
+
const names = raw.filter((t) => typeof t === "string");
|
|
248
|
+
return names.length > 0 ? names : null;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function validateValue(value, schema, path, issues, depth) {
|
|
253
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
254
|
+
if (typeof schema === "string") {
|
|
255
|
+
if (PRIMITIVE_TYPE_NAMES.has(schema) && !matchesType(value, schema)) {
|
|
256
|
+
issues.push({
|
|
257
|
+
path,
|
|
258
|
+
keyword: "type",
|
|
259
|
+
expected: schema,
|
|
260
|
+
received: render(value),
|
|
261
|
+
message: `Parameter "${path}" must be of type ${schema}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!isPlainObject(schema)) return;
|
|
267
|
+
const types = readTypes(schema);
|
|
268
|
+
if (types !== null && !types.some((t) => matchesType(value, t))) {
|
|
269
|
+
const expected = types.join(" | ");
|
|
270
|
+
issues.push({
|
|
271
|
+
path,
|
|
272
|
+
keyword: "type",
|
|
273
|
+
expected,
|
|
274
|
+
received: render(value),
|
|
275
|
+
message: `Parameter "${path}" must be of type ${expected}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (hasOwn(schema, "const") && !jsonEquals(value, schema["const"])) {
|
|
280
|
+
const expected = renderJson(schema["const"]);
|
|
281
|
+
const received = renderJson(value);
|
|
282
|
+
issues.push({
|
|
283
|
+
path,
|
|
284
|
+
keyword: "const",
|
|
285
|
+
expected,
|
|
286
|
+
received,
|
|
287
|
+
message: `Parameter "${path}" must equal ${expected}, received ${received}.`
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const enumValues = schema["enum"];
|
|
292
|
+
if (Array.isArray(enumValues) && !enumValues.some((allowed) => jsonEquals(value, allowed))) {
|
|
293
|
+
const expected = enumValues.map((v) => renderJson(v)).join(", ");
|
|
294
|
+
const received = renderJson(value);
|
|
295
|
+
issues.push({
|
|
296
|
+
path,
|
|
297
|
+
keyword: "enum",
|
|
298
|
+
expected,
|
|
299
|
+
received,
|
|
300
|
+
message: `Parameter "${path}" must be one of [${expected}], received ${received}.`
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (typeof value === "number") {
|
|
305
|
+
const min = schema["minimum"];
|
|
306
|
+
if (typeof min === "number" && value < min) {
|
|
307
|
+
issues.push({
|
|
308
|
+
path,
|
|
309
|
+
keyword: "minimum",
|
|
310
|
+
expected: `>= ${min}`,
|
|
311
|
+
received: render(value),
|
|
312
|
+
message: `Parameter "${path}" must be >= ${min}, received ${value}.`
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const max = schema["maximum"];
|
|
316
|
+
if (typeof max === "number" && value > max) {
|
|
317
|
+
issues.push({
|
|
318
|
+
path,
|
|
319
|
+
keyword: "maximum",
|
|
320
|
+
expected: `<= ${max}`,
|
|
321
|
+
received: render(value),
|
|
322
|
+
message: `Parameter "${path}" must be <= ${max}, received ${value}.`
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (typeof value === "string") {
|
|
327
|
+
const minLength = schema["minLength"];
|
|
328
|
+
if (typeof minLength === "number" && value.length < minLength) {
|
|
329
|
+
issues.push({
|
|
330
|
+
path,
|
|
331
|
+
keyword: "minLength",
|
|
332
|
+
expected: `length >= ${minLength}`,
|
|
333
|
+
received: `length ${value.length}`,
|
|
334
|
+
message: `Parameter "${path}" must be at least ${minLength} characters, received ${value.length}.`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const maxLength = schema["maxLength"];
|
|
338
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
339
|
+
issues.push({
|
|
340
|
+
path,
|
|
341
|
+
keyword: "maxLength",
|
|
342
|
+
expected: `length <= ${maxLength}`,
|
|
343
|
+
received: `length ${value.length}`,
|
|
344
|
+
message: `Parameter "${path}" must be at most ${maxLength} characters, received ${value.length}.`
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const pattern = schema["pattern"];
|
|
348
|
+
if (typeof pattern === "string" && pattern.length <= MAX_PATTERN_LENGTH && !hasNestedQuantifiers(pattern)) {
|
|
349
|
+
let re;
|
|
350
|
+
try {
|
|
351
|
+
re = new RegExp(pattern);
|
|
352
|
+
} catch {
|
|
353
|
+
re = null;
|
|
354
|
+
}
|
|
355
|
+
if (re !== null && !re.test(value)) {
|
|
356
|
+
issues.push({
|
|
357
|
+
path,
|
|
358
|
+
keyword: "pattern",
|
|
359
|
+
expected: `match /${pattern}/`,
|
|
360
|
+
received: render(value),
|
|
361
|
+
message: `Parameter "${path}" must match /${pattern}/, received ${render(value)}.`
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(value) && hasOwn(schema, "items")) {
|
|
367
|
+
const items = schema["items"];
|
|
368
|
+
value.forEach((element, index) => {
|
|
369
|
+
validateValue(element, items, `${path}[${index}]`, issues, depth + 1);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (isPlainObject(value) && (isPlainObject(schema["properties"]) || hasOwn(schema, "required") || schema["additionalProperties"] === false)) {
|
|
373
|
+
validateObject(value, schema, path, issues, depth);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function validateObject(value, schema, basePath, issues, depth) {
|
|
377
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
378
|
+
const properties = isPlainObject(schema["properties"]) ? schema["properties"] : {};
|
|
379
|
+
const requiredRaw = schema["required"];
|
|
380
|
+
const required = Array.isArray(requiredRaw) ? requiredRaw.filter((k) => typeof k === "string") : [];
|
|
381
|
+
for (const key of required) {
|
|
382
|
+
if (!hasOwn(value, key) || value[key] === void 0) {
|
|
383
|
+
const at = joinPath(basePath, key);
|
|
384
|
+
issues.push({
|
|
385
|
+
path: at,
|
|
386
|
+
keyword: "required",
|
|
387
|
+
expected: "present",
|
|
388
|
+
message: `Required parameter "${at}" is missing.`
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (schema["additionalProperties"] === false) {
|
|
393
|
+
const declared = new Set(Object.keys(properties));
|
|
394
|
+
const expected = declared.size === 0 ? "no parameters" : `one of [${[...declared].join(", ")}]`;
|
|
395
|
+
for (const key of Object.keys(value)) {
|
|
396
|
+
if (!declared.has(key)) {
|
|
397
|
+
const at = joinPath(basePath, key);
|
|
398
|
+
issues.push({
|
|
399
|
+
path: at,
|
|
400
|
+
keyword: "additionalProperties",
|
|
401
|
+
expected,
|
|
402
|
+
received: render(value[key]),
|
|
403
|
+
message: `Parameter "${at}" is not declared by the schema, which sets additionalProperties: false.`
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
409
|
+
if (!hasOwn(value, key) || value[key] === void 0) continue;
|
|
410
|
+
validateValue(value[key], propSchema, joinPath(basePath, key), issues, depth + 1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function isObjectSchemaForm(schema) {
|
|
414
|
+
if (isPlainObject(schema["properties"])) return true;
|
|
415
|
+
if (schema["type"] !== "object") return false;
|
|
416
|
+
return Object.keys(schema).every((key) => OBJECT_SCHEMA_KEYWORDS.has(key));
|
|
417
|
+
}
|
|
418
|
+
function normalizeMapForm(schema) {
|
|
419
|
+
if (isObjectSchemaForm(schema)) return null;
|
|
420
|
+
const properties = {};
|
|
421
|
+
for (const [key, raw] of Object.entries(schema)) {
|
|
422
|
+
if (raw === null || raw === void 0) continue;
|
|
423
|
+
if (typeof raw === "string") {
|
|
424
|
+
properties[key] = PRIMITIVE_TYPE_NAMES.has(raw) ? { type: raw } : {};
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (isPlainObject(raw)) {
|
|
428
|
+
const { optional: _optional, ...rest } = raw;
|
|
429
|
+
properties[key] = rest;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { type: "object", properties };
|
|
434
|
+
}
|
|
435
|
+
function validateActionParams(schema, params) {
|
|
436
|
+
if (!isPlainObject(schema)) return { valid: true, issues: [] };
|
|
437
|
+
const normalized = normalizeMapForm(schema) ?? schema;
|
|
438
|
+
const hasProperties = isPlainObject(normalized["properties"]);
|
|
439
|
+
const hasRequired = Array.isArray(normalized["required"]);
|
|
440
|
+
const forbidsExtras = normalized["additionalProperties"] === false;
|
|
441
|
+
if (!hasProperties && !hasRequired && !forbidsExtras) {
|
|
442
|
+
return { valid: true, issues: [] };
|
|
443
|
+
}
|
|
444
|
+
const issues = [];
|
|
445
|
+
if (params === void 0 || params === null) {
|
|
446
|
+
validateObject({}, normalized, "", issues, 0);
|
|
447
|
+
return { valid: issues.length === 0, issues };
|
|
448
|
+
}
|
|
449
|
+
if (!isPlainObject(params)) {
|
|
450
|
+
issues.push({
|
|
451
|
+
path: "",
|
|
452
|
+
keyword: "type",
|
|
453
|
+
expected: "object",
|
|
454
|
+
received: render(params),
|
|
455
|
+
message: `Action params must be an object, received ${jsonTypeOf(params)} (${render(params)}).`
|
|
456
|
+
});
|
|
457
|
+
return { valid: false, issues };
|
|
458
|
+
}
|
|
459
|
+
validateObject(params, normalized, "", issues, 0);
|
|
460
|
+
return { valid: issues.length === 0, issues };
|
|
461
|
+
}
|
|
462
|
+
function formatParamValidationFailure(componentId, actionId, issues) {
|
|
463
|
+
const detail = issues.map((i) => i.message).join(" ");
|
|
464
|
+
return `Action "${actionId}" on component "${componentId}" was rejected: params do not match the action's declared paramSchema. ${detail}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
10
467
|
// src/control/action-executor.ts
|
|
11
468
|
var DEFAULT_WAIT_OPTIONS = {
|
|
12
469
|
visible: true,
|
|
@@ -16,6 +473,68 @@ var DEFAULT_WAIT_OPTIONS = {
|
|
|
16
473
|
timeout: 1e4,
|
|
17
474
|
interval: 100
|
|
18
475
|
};
|
|
476
|
+
var NativeActionError = class extends Error {
|
|
477
|
+
constructor(message, code) {
|
|
478
|
+
super(message);
|
|
479
|
+
this.code = code;
|
|
480
|
+
this.name = "NativeActionError";
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
var DEFAULT_SCROLL_INTO_VIEW_PADDING = 16;
|
|
484
|
+
var SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS = 1e3;
|
|
485
|
+
var SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS = 250;
|
|
486
|
+
function measurePageRectNow(element, preferred) {
|
|
487
|
+
const preferredNode = preferred;
|
|
488
|
+
const node = preferredNode && typeof preferredNode.measureInWindow === "function" ? preferredNode : element.ref.current;
|
|
489
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
490
|
+
const state = element.getState();
|
|
491
|
+
return Promise.resolve(state.visible ? pageRectOf(state) : null);
|
|
492
|
+
}
|
|
493
|
+
return new Promise((resolve) => {
|
|
494
|
+
let settled = false;
|
|
495
|
+
const done = (rect) => {
|
|
496
|
+
if (settled) return;
|
|
497
|
+
settled = true;
|
|
498
|
+
clearTimeout(timer);
|
|
499
|
+
resolve(rect);
|
|
500
|
+
};
|
|
501
|
+
const timer = setTimeout(() => done(null), SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS);
|
|
502
|
+
try {
|
|
503
|
+
node.measureInWindow(
|
|
504
|
+
(x, y, w, h) => done(
|
|
505
|
+
[x, y, w, h].every(Number.isFinite) && w > 0 && h > 0 ? { left: x, top: y, right: x + w, bottom: y + h } : null
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
} catch {
|
|
509
|
+
done(null);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function resolveScrollView(current) {
|
|
514
|
+
const c = current;
|
|
515
|
+
if (!c) return null;
|
|
516
|
+
if (typeof c.scrollTo === "function")
|
|
517
|
+
return c;
|
|
518
|
+
for (const unwrap of [c.getNativeScrollRef, c.getScrollResponder]) {
|
|
519
|
+
if (typeof unwrap !== "function") continue;
|
|
520
|
+
let inner;
|
|
521
|
+
try {
|
|
522
|
+
inner = unwrap.call(c);
|
|
523
|
+
} catch {
|
|
524
|
+
inner = null;
|
|
525
|
+
}
|
|
526
|
+
if (inner && typeof inner.scrollTo === "function")
|
|
527
|
+
return inner;
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
function isDeclaredHorizontal(registeredProps, ...refs) {
|
|
532
|
+
if (registeredProps?.horizontal === true) return true;
|
|
533
|
+
return refs.some((r) => {
|
|
534
|
+
const props = r?.props;
|
|
535
|
+
return props !== null && typeof props === "object" && props.horizontal === true;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
19
538
|
function sleep(ms) {
|
|
20
539
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
540
|
}
|
|
@@ -135,6 +654,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
135
654
|
return {
|
|
136
655
|
success: false,
|
|
137
656
|
error: errorMsg,
|
|
657
|
+
...error instanceof NativeActionError ? { code: error.code } : {},
|
|
138
658
|
stack: error instanceof Error ? error.stack : void 0,
|
|
139
659
|
durationMs: failTime - startTime,
|
|
140
660
|
timestamp: failTime,
|
|
@@ -152,7 +672,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
152
672
|
}
|
|
153
673
|
const props = element.props || {};
|
|
154
674
|
if (element.customActions && action in element.customActions) {
|
|
155
|
-
return element.customActions[action].handler(params);
|
|
675
|
+
return element.customActions[action].handler(params, { signal: inertAbortSignal() });
|
|
156
676
|
}
|
|
157
677
|
switch (action) {
|
|
158
678
|
case "click":
|
|
@@ -175,6 +695,11 @@ var DefaultNativeActionExecutor = class {
|
|
|
175
695
|
return this.performBlur(element);
|
|
176
696
|
case "scroll":
|
|
177
697
|
return this.performScroll(props, params);
|
|
698
|
+
case "scrollIntoView":
|
|
699
|
+
return this.performScrollIntoView(
|
|
700
|
+
element,
|
|
701
|
+
params
|
|
702
|
+
);
|
|
178
703
|
case "swipe":
|
|
179
704
|
return this.performSwipe(props, params);
|
|
180
705
|
case "toggle":
|
|
@@ -341,6 +866,138 @@ var DefaultNativeActionExecutor = class {
|
|
|
341
866
|
props.onScroll(event);
|
|
342
867
|
}
|
|
343
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Perform scrollIntoView: scroll the element's DECLARED scroll container so
|
|
871
|
+
* the element sits `padding` dp below the top of its viewport.
|
|
872
|
+
*
|
|
873
|
+
* The container comes from `scrollAncestorId` and nowhere else — the
|
|
874
|
+
* registry keeps no parent chain (`RegisterElementOptions.scrollAncestorId`),
|
|
875
|
+
* so there is no "nearest ScrollView" to find. Nothing here tracks a
|
|
876
|
+
* ScrollView's content offset either (`performScroll` only fakes an
|
|
877
|
+
* `onScroll` event), so the target is measured CONTENT-relative with
|
|
878
|
+
* `measureLayout` against the container's inner content view: stateless,
|
|
879
|
+
* and immune to a stale-offset race.
|
|
880
|
+
*
|
|
881
|
+
* Result shape mirrors the web executor's `scrollIntoView`
|
|
882
|
+
* (`@qontinui/ui-bridge` `control/action-executor.ts`):
|
|
883
|
+
* `{ alreadyVisible, scrolled }`, short-circuiting when the element is
|
|
884
|
+
* already fully inside its clip region. "Fully visible" is only claimed when
|
|
885
|
+
* it is MEASURED — a page-space rect for the element and a known,
|
|
886
|
+
* non-empty clip (window ∩ declared container), both measured at call
|
|
887
|
+
* time. Anything unknown scrolls.
|
|
888
|
+
*
|
|
889
|
+
* Containers: a ScrollView, or a VirtualizedList (FlatList / SectionList)
|
|
890
|
+
* unwrapped through `getNativeScrollRef()` / `getScrollResponder()`.
|
|
891
|
+
* Vertical only — a `horizontal` container is NOT_SUPPORTED. The action
|
|
892
|
+
* returns once `scrollTo` is dispatched; positions settle on the next frame.
|
|
893
|
+
*
|
|
894
|
+
* Prerequisites are checked BEFORE the visibility short-circuit, so whether
|
|
895
|
+
* the element supports the action does not depend on where it happens to be
|
|
896
|
+
* on screen. Each missing prerequisite is a `NOT_SUPPORTED` failure.
|
|
897
|
+
*/
|
|
898
|
+
async performScrollIntoView(element, params) {
|
|
899
|
+
const padding = params?.padding ?? DEFAULT_SCROLL_INTO_VIEW_PADDING;
|
|
900
|
+
if (typeof padding !== "number" || !Number.isFinite(padding) || padding < 0) {
|
|
901
|
+
throw new Error('scrollIntoView "padding" must be a finite, non-negative number');
|
|
902
|
+
}
|
|
903
|
+
const ancestorId = element.scrollAncestorId;
|
|
904
|
+
if (!ancestorId) {
|
|
905
|
+
throw new NativeActionError(
|
|
906
|
+
`scrollIntoView is not supported on element "${element.id}": it declares no scrollAncestorId, and the registry keeps no parent chain to find its scroll container from`,
|
|
907
|
+
"NOT_SUPPORTED"
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const ancestor = ancestorId === element.id ? void 0 : this.registry.getElement(ancestorId);
|
|
911
|
+
if (!ancestor) {
|
|
912
|
+
throw new NativeActionError(
|
|
913
|
+
`scrollIntoView is not supported on element "${element.id}": its declared scrollAncestorId "${ancestorId}" is not a registered element`,
|
|
914
|
+
"NOT_SUPPORTED"
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
const container = resolveScrollView(ancestor.ref.current);
|
|
918
|
+
if (!container) {
|
|
919
|
+
throw new NativeActionError(
|
|
920
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" has no scrollTo (not a mounted ScrollView, and no getNativeScrollRef/getScrollResponder yielding one)`,
|
|
921
|
+
"NOT_SUPPORTED"
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (isDeclaredHorizontal(ancestor.props, ancestor.ref.current, container)) {
|
|
925
|
+
throw new NativeActionError(
|
|
926
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" is horizontal (only vertical containers are supported)`,
|
|
927
|
+
"NOT_SUPPORTED"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const innerViewRef = container.getInnerViewRef?.();
|
|
931
|
+
const innerView = innerViewRef ?? container.getInnerViewNode?.();
|
|
932
|
+
const innerViewIsTag = innerViewRef == null && typeof innerView === "number";
|
|
933
|
+
if (innerView === void 0 || innerView === null) {
|
|
934
|
+
throw new NativeActionError(
|
|
935
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" exposes no inner content view to measure against`,
|
|
936
|
+
"NOT_SUPPORTED"
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const target = element.ref.current;
|
|
940
|
+
if (!target || typeof target.measureLayout !== "function") {
|
|
941
|
+
throw new NativeActionError(
|
|
942
|
+
`scrollIntoView is not supported on element "${element.id}": its ref has no measureLayout (not a mounted host view)`,
|
|
943
|
+
"NOT_SUPPORTED"
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
const [rect, ancestorRect] = await Promise.all([
|
|
947
|
+
measurePageRectNow(element),
|
|
948
|
+
measurePageRectNow(ancestor, container)
|
|
949
|
+
]);
|
|
950
|
+
const viewport = this.registry.getViewportRect();
|
|
951
|
+
const clip = ancestorRect ? intersectRects(viewport, ancestorRect) : null;
|
|
952
|
+
if (rect && clip && !isEmptyRect(clip) && rect.right > rect.left && rect.bottom > rect.top && rect.top >= clip.top && rect.left >= clip.left && rect.bottom <= clip.bottom && rect.right <= clip.right) {
|
|
953
|
+
return { alreadyVisible: true, scrolled: false };
|
|
954
|
+
}
|
|
955
|
+
const measured = await new Promise(
|
|
956
|
+
(resolve, reject) => {
|
|
957
|
+
let settled = false;
|
|
958
|
+
const timer = setTimeout(() => {
|
|
959
|
+
if (settled) return;
|
|
960
|
+
settled = true;
|
|
961
|
+
reject(
|
|
962
|
+
new Error(
|
|
963
|
+
`scrollIntoView: measureLayout did not call back within ${SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS}ms` + (innerViewIsTag ? " (measured against a numeric inner-view tag from getInnerViewNode, which the new architecture does not support)" : "")
|
|
964
|
+
)
|
|
965
|
+
);
|
|
966
|
+
}, SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS);
|
|
967
|
+
const finish = (fn) => {
|
|
968
|
+
if (settled) return;
|
|
969
|
+
settled = true;
|
|
970
|
+
clearTimeout(timer);
|
|
971
|
+
fn();
|
|
972
|
+
};
|
|
973
|
+
try {
|
|
974
|
+
target.measureLayout(
|
|
975
|
+
innerView,
|
|
976
|
+
(x, y, width) => finish(
|
|
977
|
+
() => Number.isFinite(y) ? resolve({ x, y, width }) : reject(new Error("scrollIntoView: measureLayout returned a non-finite y"))
|
|
978
|
+
),
|
|
979
|
+
() => finish(
|
|
980
|
+
() => reject(
|
|
981
|
+
new Error(
|
|
982
|
+
`scrollIntoView: measureLayout failed for "${element.id}" relative to "${ancestorId}"`
|
|
983
|
+
)
|
|
984
|
+
)
|
|
985
|
+
)
|
|
986
|
+
);
|
|
987
|
+
} catch (error) {
|
|
988
|
+
finish(() => reject(error));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
if (ancestorRect && Number.isFinite(measured.x) && Number.isFinite(measured.width) && measured.x + measured.width > ancestorRect.right - ancestorRect.left + 1) {
|
|
993
|
+
throw new NativeActionError(
|
|
994
|
+
`scrollIntoView is not supported on element "${element.id}": it is laid out beyond the horizontal extent of its scroll ancestor "${ancestorId}" (sideways overflow, a horizontal container, or a nested horizontal container \u2014 declare that inner container as its scrollAncestorId); only vertical scrolling is supported`,
|
|
995
|
+
"NOT_SUPPORTED"
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
container.scrollTo({ y: Math.max(0, measured.y - padding), animated: false });
|
|
999
|
+
return { alreadyVisible: false, scrolled: true };
|
|
1000
|
+
}
|
|
344
1001
|
/**
|
|
345
1002
|
* Perform swipe action
|
|
346
1003
|
*/
|
|
@@ -395,7 +1052,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
395
1052
|
/**
|
|
396
1053
|
* Execute a component action
|
|
397
1054
|
*/
|
|
398
|
-
async executeComponentAction(componentId, request) {
|
|
1055
|
+
async executeComponentAction(componentId, request, options = {}) {
|
|
399
1056
|
const startTime = Date.now();
|
|
400
1057
|
try {
|
|
401
1058
|
const component = this.registry.getComponent(componentId);
|
|
@@ -418,10 +1075,71 @@ var DefaultNativeActionExecutor = class {
|
|
|
418
1075
|
requestId: request.requestId
|
|
419
1076
|
};
|
|
420
1077
|
}
|
|
421
|
-
const
|
|
1078
|
+
const timeout = normalizeActionTimeoutMs(request.timeoutMs);
|
|
1079
|
+
if (!timeout.ok) {
|
|
1080
|
+
return {
|
|
1081
|
+
success: false,
|
|
1082
|
+
error: `Action "${request.action}" on component "${componentId}" was rejected: ${timeout.reason}.`,
|
|
1083
|
+
durationMs: Date.now() - startTime,
|
|
1084
|
+
timestamp: Date.now(),
|
|
1085
|
+
requestId: request.requestId
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
const timeoutMs = timeout.timeoutMs;
|
|
1089
|
+
const validationMode = options.paramValidation ?? getDefaultParamValidationMode();
|
|
1090
|
+
if (validationMode !== "off" && action.paramSchema !== void 0) {
|
|
1091
|
+
let validation;
|
|
1092
|
+
try {
|
|
1093
|
+
validation = validateActionParams(action.paramSchema, request.params);
|
|
1094
|
+
} catch (validatorFault) {
|
|
1095
|
+
const detail = validatorFault instanceof Error ? validatorFault.message : String(validatorFault);
|
|
1096
|
+
const faultMessage = `Action "${request.action}" on component "${componentId}": its declared paramSchema could not be evaluated (${detail}).`;
|
|
1097
|
+
if (validationMode === "enforce") {
|
|
1098
|
+
return {
|
|
1099
|
+
success: false,
|
|
1100
|
+
error: faultMessage,
|
|
1101
|
+
durationMs: Date.now() - startTime,
|
|
1102
|
+
timestamp: Date.now(),
|
|
1103
|
+
requestId: request.requestId
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
console.warn(`[ui-bridge-native] ${faultMessage}`);
|
|
1107
|
+
validation = { valid: true, issues: [] };
|
|
1108
|
+
}
|
|
1109
|
+
if (!validation.valid) {
|
|
1110
|
+
const message = formatParamValidationFailure(
|
|
1111
|
+
componentId,
|
|
1112
|
+
request.action,
|
|
1113
|
+
validation.issues
|
|
1114
|
+
);
|
|
1115
|
+
if (validationMode === "enforce") {
|
|
1116
|
+
return {
|
|
1117
|
+
success: false,
|
|
1118
|
+
error: message,
|
|
1119
|
+
durationMs: Date.now() - startTime,
|
|
1120
|
+
timestamp: Date.now(),
|
|
1121
|
+
requestId: request.requestId
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
console.warn(`[ui-bridge-native] ${message}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const outcome = await runAbortable((signal) => action.handler(request.params, { signal }), {
|
|
1128
|
+
signal: options.signal,
|
|
1129
|
+
timeoutMs
|
|
1130
|
+
});
|
|
1131
|
+
if (outcome.aborted) {
|
|
1132
|
+
return {
|
|
1133
|
+
success: false,
|
|
1134
|
+
error: outcome.reason === "timeout" ? `Action "${request.action}" on component "${componentId}" was abandoned after its ${timeoutMs}ms timeout elapsed.` : `Action "${request.action}" on component "${componentId}" was cancelled by the caller's abort signal.`,
|
|
1135
|
+
durationMs: Date.now() - startTime,
|
|
1136
|
+
timestamp: Date.now(),
|
|
1137
|
+
requestId: request.requestId
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
422
1140
|
return {
|
|
423
1141
|
success: true,
|
|
424
|
-
result,
|
|
1142
|
+
result: outcome.result,
|
|
425
1143
|
durationMs: Date.now() - startTime,
|
|
426
1144
|
timestamp: Date.now(),
|
|
427
1145
|
requestId: request.requestId
|
|
@@ -543,6 +1261,7 @@ function createNativeActionExecutor(registry) {
|
|
|
543
1261
|
}
|
|
544
1262
|
|
|
545
1263
|
exports.DefaultNativeActionExecutor = DefaultNativeActionExecutor;
|
|
1264
|
+
exports.NativeActionError = NativeActionError;
|
|
546
1265
|
exports.createNativeActionExecutor = createNativeActionExecutor;
|
|
547
1266
|
//# sourceMappingURL=index.js.map
|
|
548
1267
|
//# sourceMappingURL=index.js.map
|