@qontinui/ui-bridge 0.1.1
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/index.d.mts +134 -0
- package/dist/control/index.d.ts +134 -0
- package/dist/control/index.js +924 -0
- package/dist/control/index.js.map +1 -0
- package/dist/control/index.mjs +919 -0
- package/dist/control/index.mjs.map +1 -0
- package/dist/core/index.d.mts +52 -0
- package/dist/core/index.d.ts +52 -0
- package/dist/core/index.js +1424 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +1409 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/debug/index.d.mts +93 -0
- package/dist/debug/index.d.ts +93 -0
- package/dist/debug/index.js +673 -0
- package/dist/debug/index.js.map +1 -0
- package/dist/debug/index.mjs +664 -0
- package/dist/debug/index.mjs.map +1 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +4719 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4665 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metrics-BCG7z7Aq.d.mts +147 -0
- package/dist/metrics-QCnK0EFw.d.ts +147 -0
- package/dist/react/index.d.mts +786 -0
- package/dist/react/index.d.ts +786 -0
- package/dist/react/index.js +4312 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +4290 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/registry-CT6BVVKr.d.mts +253 -0
- package/dist/registry-D4mQ01B3.d.ts +253 -0
- package/dist/render-log/index.d.mts +340 -0
- package/dist/render-log/index.d.ts +340 -0
- package/dist/render-log/index.js +702 -0
- package/dist/render-log/index.js.map +1 -0
- package/dist/render-log/index.mjs +695 -0
- package/dist/render-log/index.mjs.map +1 -0
- package/dist/types-BDkXy5si.d.ts +354 -0
- package/dist/types-BpvpStn3.d.mts +802 -0
- package/dist/types-BpvpStn3.d.ts +802 -0
- package/dist/types-DdJD9yw5.d.mts +354 -0
- package/dist/websocket-client-B2LC9CYc.d.mts +124 -0
- package/dist/websocket-client-DupH0X7B.d.ts +124 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
// src/core/element-identifier.ts
|
|
2
|
+
var ID_ATTRIBUTES = ["data-ui-id", "data-testid", "data-awas-element", "id"];
|
|
3
|
+
function generateXPath(element) {
|
|
4
|
+
if (element.id) {
|
|
5
|
+
return `//*[@id="${element.id}"]`;
|
|
6
|
+
}
|
|
7
|
+
const parts = [];
|
|
8
|
+
let current = element;
|
|
9
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
10
|
+
let selector = current.nodeName.toLowerCase();
|
|
11
|
+
const uiId = current.getAttribute("data-ui-id");
|
|
12
|
+
if (uiId) {
|
|
13
|
+
selector += `[@data-ui-id="${uiId}"]`;
|
|
14
|
+
parts.unshift(selector);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
const testId = current.getAttribute("data-testid");
|
|
18
|
+
if (testId) {
|
|
19
|
+
selector += `[@data-testid="${testId}"]`;
|
|
20
|
+
parts.unshift(selector);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
const id = current.id;
|
|
24
|
+
if (id) {
|
|
25
|
+
selector += `[@id="${id}"]`;
|
|
26
|
+
parts.unshift(selector);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
const parentEl = current.parentElement;
|
|
30
|
+
if (parentEl) {
|
|
31
|
+
const currentEl = current;
|
|
32
|
+
const siblings = Array.from(parentEl.children).filter(
|
|
33
|
+
(child) => child.nodeName === currentEl.nodeName
|
|
34
|
+
);
|
|
35
|
+
if (siblings.length > 1) {
|
|
36
|
+
const index = siblings.indexOf(currentEl) + 1;
|
|
37
|
+
selector += `[${index}]`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
parts.unshift(selector);
|
|
41
|
+
current = parentEl;
|
|
42
|
+
}
|
|
43
|
+
return "/" + parts.join("/");
|
|
44
|
+
}
|
|
45
|
+
function generateCSSSelector(element) {
|
|
46
|
+
const uiId = element.getAttribute("data-ui-id");
|
|
47
|
+
if (uiId) {
|
|
48
|
+
return `[data-ui-id="${uiId}"]`;
|
|
49
|
+
}
|
|
50
|
+
const testId = element.getAttribute("data-testid");
|
|
51
|
+
if (testId) {
|
|
52
|
+
return `[data-testid="${testId}"]`;
|
|
53
|
+
}
|
|
54
|
+
const awasId = element.getAttribute("data-awas-element");
|
|
55
|
+
if (awasId) {
|
|
56
|
+
return `[data-awas-element="${awasId}"]`;
|
|
57
|
+
}
|
|
58
|
+
if (element.id) {
|
|
59
|
+
return `#${CSS.escape(element.id)}`;
|
|
60
|
+
}
|
|
61
|
+
const path = [];
|
|
62
|
+
let current = element;
|
|
63
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
64
|
+
let selector = current.nodeName.toLowerCase();
|
|
65
|
+
const parentUiId = current.getAttribute("data-ui-id");
|
|
66
|
+
if (parentUiId && current !== element) {
|
|
67
|
+
path.unshift(`[data-ui-id="${parentUiId}"]`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const parentTestId = current.getAttribute("data-testid");
|
|
71
|
+
if (parentTestId && current !== element) {
|
|
72
|
+
path.unshift(`[data-testid="${parentTestId}"]`);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (current.id) {
|
|
76
|
+
path.unshift(`#${CSS.escape(current.id)}`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const parentEl = current.parentElement;
|
|
80
|
+
if (parentEl) {
|
|
81
|
+
const currentEl = current;
|
|
82
|
+
const siblings = Array.from(parentEl.children);
|
|
83
|
+
const sameTagSiblings = siblings.filter(
|
|
84
|
+
(s) => s.nodeName === currentEl.nodeName
|
|
85
|
+
);
|
|
86
|
+
if (sameTagSiblings.length > 1) {
|
|
87
|
+
const index = siblings.indexOf(currentEl) + 1;
|
|
88
|
+
selector += `:nth-child(${index})`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
path.unshift(selector);
|
|
92
|
+
current = current.parentElement;
|
|
93
|
+
}
|
|
94
|
+
return path.join(" > ");
|
|
95
|
+
}
|
|
96
|
+
function getBestIdentifier(element) {
|
|
97
|
+
const uiId = element.getAttribute("data-ui-id");
|
|
98
|
+
if (uiId) return uiId;
|
|
99
|
+
const testId = element.getAttribute("data-testid");
|
|
100
|
+
if (testId) return testId;
|
|
101
|
+
const awasId = element.getAttribute("data-awas-element");
|
|
102
|
+
if (awasId) return awasId;
|
|
103
|
+
if (element.id) return element.id;
|
|
104
|
+
return generateCSSSelector(element);
|
|
105
|
+
}
|
|
106
|
+
function createElementIdentifier(element) {
|
|
107
|
+
return {
|
|
108
|
+
uiId: element.getAttribute("data-ui-id") || void 0,
|
|
109
|
+
testId: element.getAttribute("data-testid") || void 0,
|
|
110
|
+
awasId: element.getAttribute("data-awas-element") || void 0,
|
|
111
|
+
htmlId: element.id || void 0,
|
|
112
|
+
xpath: generateXPath(element),
|
|
113
|
+
selector: generateCSSSelector(element)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function findElementByIdentifier(identifier, root = document) {
|
|
117
|
+
if (typeof identifier === "string") {
|
|
118
|
+
const byUiId = root.querySelector(`[data-ui-id="${identifier}"]`);
|
|
119
|
+
if (byUiId) return byUiId;
|
|
120
|
+
const byTestId = root.querySelector(`[data-testid="${identifier}"]`);
|
|
121
|
+
if (byTestId) return byTestId;
|
|
122
|
+
const byAwasId = root.querySelector(`[data-awas-element="${identifier}"]`);
|
|
123
|
+
if (byAwasId) return byAwasId;
|
|
124
|
+
const byId = root.querySelector(`#${CSS.escape(identifier)}`);
|
|
125
|
+
if (byId) return byId;
|
|
126
|
+
try {
|
|
127
|
+
const bySelector = root.querySelector(identifier);
|
|
128
|
+
if (bySelector) return bySelector;
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const result = document.evaluate(
|
|
133
|
+
identifier,
|
|
134
|
+
root,
|
|
135
|
+
null,
|
|
136
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
137
|
+
null
|
|
138
|
+
);
|
|
139
|
+
if (result.singleNodeValue instanceof HTMLElement) {
|
|
140
|
+
return result.singleNodeValue;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (identifier.uiId) {
|
|
147
|
+
const el = root.querySelector(`[data-ui-id="${identifier.uiId}"]`);
|
|
148
|
+
if (el) return el;
|
|
149
|
+
}
|
|
150
|
+
if (identifier.testId) {
|
|
151
|
+
const el = root.querySelector(`[data-testid="${identifier.testId}"]`);
|
|
152
|
+
if (el) return el;
|
|
153
|
+
}
|
|
154
|
+
if (identifier.awasId) {
|
|
155
|
+
const el = root.querySelector(`[data-awas-element="${identifier.awasId}"]`);
|
|
156
|
+
if (el) return el;
|
|
157
|
+
}
|
|
158
|
+
if (identifier.htmlId) {
|
|
159
|
+
const el = root.querySelector(`#${CSS.escape(identifier.htmlId)}`);
|
|
160
|
+
if (el) return el;
|
|
161
|
+
}
|
|
162
|
+
if (identifier.selector) {
|
|
163
|
+
try {
|
|
164
|
+
const el = root.querySelector(identifier.selector);
|
|
165
|
+
if (el) return el;
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (identifier.xpath) {
|
|
170
|
+
try {
|
|
171
|
+
const result = document.evaluate(
|
|
172
|
+
identifier.xpath,
|
|
173
|
+
root,
|
|
174
|
+
null,
|
|
175
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
176
|
+
null
|
|
177
|
+
);
|
|
178
|
+
if (result.singleNodeValue instanceof HTMLElement) {
|
|
179
|
+
return result.singleNodeValue;
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function findAllElementsByIdentifier(pattern, root = document) {
|
|
187
|
+
const results = [];
|
|
188
|
+
try {
|
|
189
|
+
const elements = root.querySelectorAll(pattern);
|
|
190
|
+
results.push(...Array.from(elements));
|
|
191
|
+
if (results.length > 0) return results;
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
const partials = [
|
|
195
|
+
`[data-ui-id*="${pattern}"]`,
|
|
196
|
+
`[data-testid*="${pattern}"]`,
|
|
197
|
+
`[data-awas-element*="${pattern}"]`,
|
|
198
|
+
`[id*="${pattern}"]`
|
|
199
|
+
];
|
|
200
|
+
for (const selector of partials) {
|
|
201
|
+
try {
|
|
202
|
+
const elements = root.querySelectorAll(selector);
|
|
203
|
+
for (const el of elements) {
|
|
204
|
+
if (!results.includes(el)) {
|
|
205
|
+
results.push(el);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
function elementMatchesIdentifier(element, identifier) {
|
|
214
|
+
if (typeof identifier === "string") {
|
|
215
|
+
return element.getAttribute("data-ui-id") === identifier || element.getAttribute("data-testid") === identifier || element.getAttribute("data-awas-element") === identifier || element.id === identifier || element.matches(identifier);
|
|
216
|
+
}
|
|
217
|
+
return identifier.uiId && element.getAttribute("data-ui-id") === identifier.uiId || identifier.testId && element.getAttribute("data-testid") === identifier.testId || identifier.awasId && element.getAttribute("data-awas-element") === identifier.awasId || identifier.htmlId && element.id === identifier.htmlId || false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/core/registry.ts
|
|
221
|
+
function getElementState(element) {
|
|
222
|
+
const rect = element.getBoundingClientRect();
|
|
223
|
+
const computedStyle = window.getComputedStyle(element);
|
|
224
|
+
const state = {
|
|
225
|
+
visible: isElementVisible(element, rect, computedStyle),
|
|
226
|
+
enabled: !isElementDisabled(element),
|
|
227
|
+
focused: document.activeElement === element,
|
|
228
|
+
rect: {
|
|
229
|
+
x: rect.x,
|
|
230
|
+
y: rect.y,
|
|
231
|
+
width: rect.width,
|
|
232
|
+
height: rect.height,
|
|
233
|
+
top: rect.top,
|
|
234
|
+
right: rect.right,
|
|
235
|
+
bottom: rect.bottom,
|
|
236
|
+
left: rect.left
|
|
237
|
+
},
|
|
238
|
+
textContent: element.textContent?.trim() || void 0,
|
|
239
|
+
computedStyles: {
|
|
240
|
+
display: computedStyle.display,
|
|
241
|
+
visibility: computedStyle.visibility,
|
|
242
|
+
opacity: computedStyle.opacity,
|
|
243
|
+
pointerEvents: computedStyle.pointerEvents
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
if (element instanceof HTMLInputElement) {
|
|
247
|
+
state.value = element.value;
|
|
248
|
+
if (element.type === "checkbox" || element.type === "radio") {
|
|
249
|
+
state.checked = element.checked;
|
|
250
|
+
}
|
|
251
|
+
} else if (element instanceof HTMLTextAreaElement) {
|
|
252
|
+
state.value = element.value;
|
|
253
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
254
|
+
state.value = element.value;
|
|
255
|
+
state.selectedOptions = Array.from(element.selectedOptions).map((opt) => opt.value);
|
|
256
|
+
}
|
|
257
|
+
return state;
|
|
258
|
+
}
|
|
259
|
+
function isElementVisible(element, rect, style) {
|
|
260
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
261
|
+
if (style.display === "none") return false;
|
|
262
|
+
if (style.visibility === "hidden") return false;
|
|
263
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
264
|
+
const inViewport = rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
|
|
265
|
+
return inViewport;
|
|
266
|
+
}
|
|
267
|
+
function isElementDisabled(element) {
|
|
268
|
+
if ("disabled" in element && element.disabled) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if (element.getAttribute("aria-disabled") === "true") {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
function inferActions(type) {
|
|
277
|
+
const baseActions = ["focus", "blur", "hover"];
|
|
278
|
+
switch (type) {
|
|
279
|
+
case "button":
|
|
280
|
+
return [...baseActions, "click", "doubleClick", "rightClick"];
|
|
281
|
+
case "input":
|
|
282
|
+
return [...baseActions, "click", "type", "clear"];
|
|
283
|
+
case "textarea":
|
|
284
|
+
return [...baseActions, "click", "type", "clear"];
|
|
285
|
+
case "select":
|
|
286
|
+
return [...baseActions, "click", "select"];
|
|
287
|
+
case "checkbox":
|
|
288
|
+
return [...baseActions, "click", "check", "uncheck", "toggle"];
|
|
289
|
+
case "radio":
|
|
290
|
+
return [...baseActions, "click", "check"];
|
|
291
|
+
case "link":
|
|
292
|
+
return [...baseActions, "click"];
|
|
293
|
+
case "form":
|
|
294
|
+
return ["focus", "blur"];
|
|
295
|
+
case "menu":
|
|
296
|
+
case "menuitem":
|
|
297
|
+
return [...baseActions, "click"];
|
|
298
|
+
case "tab":
|
|
299
|
+
return [...baseActions, "click"];
|
|
300
|
+
case "dialog":
|
|
301
|
+
return ["focus", "blur"];
|
|
302
|
+
case "custom":
|
|
303
|
+
default:
|
|
304
|
+
return [...baseActions, "click"];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function inferElementType(element) {
|
|
308
|
+
const tagName = element.tagName.toLowerCase();
|
|
309
|
+
const role = element.getAttribute("role");
|
|
310
|
+
if (role) {
|
|
311
|
+
switch (role) {
|
|
312
|
+
case "button":
|
|
313
|
+
return "button";
|
|
314
|
+
case "textbox":
|
|
315
|
+
return "input";
|
|
316
|
+
case "checkbox":
|
|
317
|
+
return "checkbox";
|
|
318
|
+
case "radio":
|
|
319
|
+
return "radio";
|
|
320
|
+
case "link":
|
|
321
|
+
return "link";
|
|
322
|
+
case "listbox":
|
|
323
|
+
case "combobox":
|
|
324
|
+
return "select";
|
|
325
|
+
case "menu":
|
|
326
|
+
return "menu";
|
|
327
|
+
case "menuitem":
|
|
328
|
+
return "menuitem";
|
|
329
|
+
case "tab":
|
|
330
|
+
return "tab";
|
|
331
|
+
case "dialog":
|
|
332
|
+
return "dialog";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
switch (tagName) {
|
|
336
|
+
case "button":
|
|
337
|
+
return "button";
|
|
338
|
+
case "input": {
|
|
339
|
+
const inputType = element.type;
|
|
340
|
+
if (inputType === "checkbox") return "checkbox";
|
|
341
|
+
if (inputType === "radio") return "radio";
|
|
342
|
+
if (inputType === "submit" || inputType === "button") return "button";
|
|
343
|
+
return "input";
|
|
344
|
+
}
|
|
345
|
+
case "textarea":
|
|
346
|
+
return "textarea";
|
|
347
|
+
case "select":
|
|
348
|
+
return "select";
|
|
349
|
+
case "a":
|
|
350
|
+
return "link";
|
|
351
|
+
case "form":
|
|
352
|
+
return "form";
|
|
353
|
+
default:
|
|
354
|
+
return "custom";
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
var UIBridgeRegistry = class {
|
|
358
|
+
constructor(options = {}) {
|
|
359
|
+
this.elements = /* @__PURE__ */ new Map();
|
|
360
|
+
this.components = /* @__PURE__ */ new Map();
|
|
361
|
+
this.workflows = /* @__PURE__ */ new Map();
|
|
362
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
363
|
+
// State management
|
|
364
|
+
this.states = /* @__PURE__ */ new Map();
|
|
365
|
+
this.stateGroups = /* @__PURE__ */ new Map();
|
|
366
|
+
this.transitions = /* @__PURE__ */ new Map();
|
|
367
|
+
this.activeStates = /* @__PURE__ */ new Set();
|
|
368
|
+
this.options = options;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Emit an event
|
|
372
|
+
*/
|
|
373
|
+
emit(type, data) {
|
|
374
|
+
const event = {
|
|
375
|
+
type,
|
|
376
|
+
timestamp: Date.now(),
|
|
377
|
+
data
|
|
378
|
+
};
|
|
379
|
+
this.options.onEvent?.(event);
|
|
380
|
+
const listeners = this.eventListeners.get(type);
|
|
381
|
+
if (listeners) {
|
|
382
|
+
for (const listener of listeners) {
|
|
383
|
+
try {
|
|
384
|
+
listener(event);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error(`Error in event listener for ${type}:`, error);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (this.options.verbose) {
|
|
391
|
+
console.log("[UIBridge]", type, data);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Register an event listener
|
|
396
|
+
*/
|
|
397
|
+
on(type, listener) {
|
|
398
|
+
if (!this.eventListeners.has(type)) {
|
|
399
|
+
this.eventListeners.set(type, /* @__PURE__ */ new Set());
|
|
400
|
+
}
|
|
401
|
+
this.eventListeners.get(type).add(listener);
|
|
402
|
+
return () => {
|
|
403
|
+
this.eventListeners.get(type)?.delete(listener);
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Remove an event listener
|
|
408
|
+
*/
|
|
409
|
+
off(type, listener) {
|
|
410
|
+
this.eventListeners.get(type)?.delete(listener);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Register an element
|
|
414
|
+
*/
|
|
415
|
+
registerElement(id, element, options = {}) {
|
|
416
|
+
const type = options.type ?? inferElementType(element);
|
|
417
|
+
const actions = options.actions ?? inferActions(type);
|
|
418
|
+
element.setAttribute("data-ui-id", id);
|
|
419
|
+
const registered = {
|
|
420
|
+
id,
|
|
421
|
+
element,
|
|
422
|
+
type,
|
|
423
|
+
label: options.label,
|
|
424
|
+
actions,
|
|
425
|
+
customActions: options.customActions,
|
|
426
|
+
getState: () => getElementState(element),
|
|
427
|
+
getIdentifier: () => createElementIdentifier(element),
|
|
428
|
+
registeredAt: Date.now(),
|
|
429
|
+
mounted: true
|
|
430
|
+
};
|
|
431
|
+
this.elements.set(id, registered);
|
|
432
|
+
this.emit("element:registered", { id, type, label: options.label });
|
|
433
|
+
return registered;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Unregister an element
|
|
437
|
+
*/
|
|
438
|
+
unregisterElement(id) {
|
|
439
|
+
const registered = this.elements.get(id);
|
|
440
|
+
if (registered) {
|
|
441
|
+
registered.mounted = false;
|
|
442
|
+
registered.element.removeAttribute("data-ui-id");
|
|
443
|
+
this.elements.delete(id);
|
|
444
|
+
this.emit("element:unregistered", { id });
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Get a registered element
|
|
451
|
+
*/
|
|
452
|
+
getElement(id) {
|
|
453
|
+
return this.elements.get(id);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get all registered elements
|
|
457
|
+
*/
|
|
458
|
+
getAllElements() {
|
|
459
|
+
return Array.from(this.elements.values());
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Find element by DOM element reference
|
|
463
|
+
*/
|
|
464
|
+
findByDOMElement(element) {
|
|
465
|
+
for (const registered of this.elements.values()) {
|
|
466
|
+
if (registered.element === element) {
|
|
467
|
+
return registered;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Register a component
|
|
474
|
+
*/
|
|
475
|
+
registerComponent(id, options) {
|
|
476
|
+
const registered = {
|
|
477
|
+
id,
|
|
478
|
+
name: options.name,
|
|
479
|
+
description: options.description,
|
|
480
|
+
actions: options.actions?.map((a) => ({
|
|
481
|
+
id: a.id,
|
|
482
|
+
label: a.label,
|
|
483
|
+
description: a.description,
|
|
484
|
+
handler: a.handler
|
|
485
|
+
})) ?? [],
|
|
486
|
+
elementIds: options.elementIds,
|
|
487
|
+
registeredAt: Date.now(),
|
|
488
|
+
mounted: true
|
|
489
|
+
};
|
|
490
|
+
this.components.set(id, registered);
|
|
491
|
+
this.emit("component:registered", { id, name: options.name });
|
|
492
|
+
return registered;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Unregister a component
|
|
496
|
+
*/
|
|
497
|
+
unregisterComponent(id) {
|
|
498
|
+
const component = this.components.get(id);
|
|
499
|
+
if (component) {
|
|
500
|
+
component.mounted = false;
|
|
501
|
+
this.components.delete(id);
|
|
502
|
+
this.emit("component:unregistered", { id });
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Get a registered component
|
|
509
|
+
*/
|
|
510
|
+
getComponent(id) {
|
|
511
|
+
return this.components.get(id);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Get all registered components
|
|
515
|
+
*/
|
|
516
|
+
getAllComponents() {
|
|
517
|
+
return Array.from(this.components.values());
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Register a workflow
|
|
521
|
+
*/
|
|
522
|
+
registerWorkflow(workflow) {
|
|
523
|
+
this.workflows.set(workflow.id, workflow);
|
|
524
|
+
return workflow;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Unregister a workflow
|
|
528
|
+
*/
|
|
529
|
+
unregisterWorkflow(id) {
|
|
530
|
+
return this.workflows.delete(id);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Get a workflow
|
|
534
|
+
*/
|
|
535
|
+
getWorkflow(id) {
|
|
536
|
+
return this.workflows.get(id);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Get all workflows
|
|
540
|
+
*/
|
|
541
|
+
getAllWorkflows() {
|
|
542
|
+
return Array.from(this.workflows.values());
|
|
543
|
+
}
|
|
544
|
+
// ==========================================================================
|
|
545
|
+
// State Management
|
|
546
|
+
// ==========================================================================
|
|
547
|
+
/**
|
|
548
|
+
* Register a state
|
|
549
|
+
*/
|
|
550
|
+
registerState(state) {
|
|
551
|
+
this.states.set(state.id, state);
|
|
552
|
+
this.emit("element:registered", { id: state.id, type: "state", name: state.name });
|
|
553
|
+
return state;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Unregister a state
|
|
557
|
+
*/
|
|
558
|
+
unregisterState(id) {
|
|
559
|
+
const state = this.states.get(id);
|
|
560
|
+
if (state) {
|
|
561
|
+
this.activeStates.delete(id);
|
|
562
|
+
this.states.delete(id);
|
|
563
|
+
this.emit("element:unregistered", { id, type: "state" });
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Get a registered state
|
|
570
|
+
*/
|
|
571
|
+
getState(id) {
|
|
572
|
+
return this.states.get(id);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Get all registered states
|
|
576
|
+
*/
|
|
577
|
+
getAllStates() {
|
|
578
|
+
return Array.from(this.states.values());
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Register a state group
|
|
582
|
+
*/
|
|
583
|
+
registerStateGroup(group) {
|
|
584
|
+
this.stateGroups.set(group.id, group);
|
|
585
|
+
return group;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Unregister a state group
|
|
589
|
+
*/
|
|
590
|
+
unregisterStateGroup(id) {
|
|
591
|
+
return this.stateGroups.delete(id);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Get a state group
|
|
595
|
+
*/
|
|
596
|
+
getStateGroup(id) {
|
|
597
|
+
return this.stateGroups.get(id);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Get all state groups
|
|
601
|
+
*/
|
|
602
|
+
getAllStateGroups() {
|
|
603
|
+
return Array.from(this.stateGroups.values());
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Register a transition
|
|
607
|
+
*/
|
|
608
|
+
registerTransition(transition) {
|
|
609
|
+
this.transitions.set(transition.id, transition);
|
|
610
|
+
return transition;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Unregister a transition
|
|
614
|
+
*/
|
|
615
|
+
unregisterTransition(id) {
|
|
616
|
+
return this.transitions.delete(id);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Get a transition
|
|
620
|
+
*/
|
|
621
|
+
getTransition(id) {
|
|
622
|
+
return this.transitions.get(id);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get all transitions
|
|
626
|
+
*/
|
|
627
|
+
getAllTransitions() {
|
|
628
|
+
return Array.from(this.transitions.values());
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get currently active states
|
|
632
|
+
*/
|
|
633
|
+
getActiveStates() {
|
|
634
|
+
return Array.from(this.activeStates);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Check if a state is active
|
|
638
|
+
*/
|
|
639
|
+
isStateActive(id) {
|
|
640
|
+
return this.activeStates.has(id);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Activate a state
|
|
644
|
+
*/
|
|
645
|
+
activateState(id) {
|
|
646
|
+
const state = this.states.get(id);
|
|
647
|
+
if (!state) {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
for (const activeId of this.activeStates) {
|
|
651
|
+
const activeState = this.states.get(activeId);
|
|
652
|
+
if (activeState?.blocking && activeState.id !== id) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
if (activeState?.blocks?.includes(id)) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const wasActive = this.activeStates.has(id);
|
|
660
|
+
this.activeStates.add(id);
|
|
661
|
+
if (!wasActive) {
|
|
662
|
+
this.emit("element:stateChanged", {
|
|
663
|
+
stateId: id,
|
|
664
|
+
active: true,
|
|
665
|
+
activeStates: this.getActiveStates()
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Deactivate a state
|
|
672
|
+
*/
|
|
673
|
+
deactivateState(id) {
|
|
674
|
+
const wasActive = this.activeStates.has(id);
|
|
675
|
+
this.activeStates.delete(id);
|
|
676
|
+
if (wasActive) {
|
|
677
|
+
this.emit("element:stateChanged", {
|
|
678
|
+
stateId: id,
|
|
679
|
+
active: false,
|
|
680
|
+
activeStates: this.getActiveStates()
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return wasActive;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Activate multiple states
|
|
687
|
+
*/
|
|
688
|
+
activateStates(ids) {
|
|
689
|
+
const activated = [];
|
|
690
|
+
for (const id of ids) {
|
|
691
|
+
if (this.activateState(id)) {
|
|
692
|
+
activated.push(id);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return activated;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Deactivate multiple states
|
|
699
|
+
*/
|
|
700
|
+
deactivateStates(ids) {
|
|
701
|
+
const deactivated = [];
|
|
702
|
+
for (const id of ids) {
|
|
703
|
+
if (this.deactivateState(id)) {
|
|
704
|
+
deactivated.push(id);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return deactivated;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Activate a state group (all states in the group)
|
|
711
|
+
*/
|
|
712
|
+
activateStateGroup(groupId) {
|
|
713
|
+
const group = this.stateGroups.get(groupId);
|
|
714
|
+
if (!group) return [];
|
|
715
|
+
return this.activateStates(group.states);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Deactivate a state group (all states in the group)
|
|
719
|
+
*/
|
|
720
|
+
deactivateStateGroup(groupId) {
|
|
721
|
+
const group = this.stateGroups.get(groupId);
|
|
722
|
+
if (!group) return [];
|
|
723
|
+
return this.deactivateStates(group.states);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Check if a transition can be executed from current state
|
|
727
|
+
*/
|
|
728
|
+
canExecuteTransition(transitionId) {
|
|
729
|
+
const transition = this.transitions.get(transitionId);
|
|
730
|
+
if (!transition) return false;
|
|
731
|
+
return transition.fromStates.some((stateId) => this.activeStates.has(stateId));
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Execute a transition
|
|
735
|
+
*/
|
|
736
|
+
async executeTransition(transitionId) {
|
|
737
|
+
const startTime = performance.now();
|
|
738
|
+
const transition = this.transitions.get(transitionId);
|
|
739
|
+
if (!transition) {
|
|
740
|
+
return {
|
|
741
|
+
success: false,
|
|
742
|
+
activatedStates: [],
|
|
743
|
+
deactivatedStates: [],
|
|
744
|
+
error: `Transition not found: ${transitionId}`,
|
|
745
|
+
durationMs: performance.now() - startTime
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (!this.canExecuteTransition(transitionId)) {
|
|
749
|
+
return {
|
|
750
|
+
success: false,
|
|
751
|
+
activatedStates: [],
|
|
752
|
+
deactivatedStates: [],
|
|
753
|
+
error: "Precondition not met: none of the fromStates are active",
|
|
754
|
+
failedPhase: "precondition",
|
|
755
|
+
durationMs: performance.now() - startTime
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
const deactivated = this.deactivateStates(transition.exitStates);
|
|
760
|
+
if (transition.exitGroups) {
|
|
761
|
+
for (const groupId of transition.exitGroups) {
|
|
762
|
+
deactivated.push(...this.deactivateStateGroup(groupId));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const activated = this.activateStates(transition.activateStates);
|
|
766
|
+
if (transition.activateGroups) {
|
|
767
|
+
for (const groupId of transition.activateGroups) {
|
|
768
|
+
activated.push(...this.activateStateGroup(groupId));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
success: true,
|
|
773
|
+
activatedStates: activated,
|
|
774
|
+
deactivatedStates: deactivated,
|
|
775
|
+
durationMs: performance.now() - startTime
|
|
776
|
+
};
|
|
777
|
+
} catch (error) {
|
|
778
|
+
return {
|
|
779
|
+
success: false,
|
|
780
|
+
activatedStates: [],
|
|
781
|
+
deactivatedStates: [],
|
|
782
|
+
error: error instanceof Error ? error.message : String(error),
|
|
783
|
+
failedPhase: "execution",
|
|
784
|
+
durationMs: performance.now() - startTime
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Find a path from current state to target states
|
|
790
|
+
*
|
|
791
|
+
* Uses a simple BFS algorithm for pathfinding.
|
|
792
|
+
* For more advanced pathfinding (Dijkstra, A*), use the Python state manager service.
|
|
793
|
+
*/
|
|
794
|
+
findPath(targetStates) {
|
|
795
|
+
if (targetStates.every((t) => this.activeStates.has(t))) {
|
|
796
|
+
return {
|
|
797
|
+
found: true,
|
|
798
|
+
transitions: [],
|
|
799
|
+
totalCost: 0,
|
|
800
|
+
targetStates,
|
|
801
|
+
estimatedSteps: 0
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
const queue = [
|
|
805
|
+
{ activeStates: new Set(this.activeStates), path: [], cost: 0 }
|
|
806
|
+
];
|
|
807
|
+
const visited = /* @__PURE__ */ new Set();
|
|
808
|
+
while (queue.length > 0) {
|
|
809
|
+
const current = queue.shift();
|
|
810
|
+
const stateKey = Array.from(current.activeStates).sort().join(",");
|
|
811
|
+
if (visited.has(stateKey)) continue;
|
|
812
|
+
visited.add(stateKey);
|
|
813
|
+
if (targetStates.every((t) => current.activeStates.has(t))) {
|
|
814
|
+
return {
|
|
815
|
+
found: true,
|
|
816
|
+
transitions: current.path,
|
|
817
|
+
totalCost: current.cost,
|
|
818
|
+
targetStates,
|
|
819
|
+
estimatedSteps: current.path.length
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
for (const transition of this.transitions.values()) {
|
|
823
|
+
const canExecute = transition.fromStates.some((s) => current.activeStates.has(s));
|
|
824
|
+
if (!canExecute) continue;
|
|
825
|
+
const newActive = new Set(current.activeStates);
|
|
826
|
+
for (const s of transition.exitStates) newActive.delete(s);
|
|
827
|
+
for (const s of transition.activateStates) newActive.add(s);
|
|
828
|
+
const newCost = current.cost + (transition.pathCost ?? 1);
|
|
829
|
+
queue.push({
|
|
830
|
+
activeStates: newActive,
|
|
831
|
+
path: [...current.path, transition.id],
|
|
832
|
+
cost: newCost
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
found: false,
|
|
838
|
+
transitions: [],
|
|
839
|
+
totalCost: 0,
|
|
840
|
+
targetStates,
|
|
841
|
+
estimatedSteps: 0
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Navigate to target states using pathfinding
|
|
846
|
+
*/
|
|
847
|
+
async navigateTo(targetStates) {
|
|
848
|
+
const startTime = performance.now();
|
|
849
|
+
const path = this.findPath(targetStates);
|
|
850
|
+
if (!path.found) {
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
path,
|
|
854
|
+
executedTransitions: [],
|
|
855
|
+
finalActiveStates: this.getActiveStates(),
|
|
856
|
+
error: `No path found to target states: ${targetStates.join(", ")}`,
|
|
857
|
+
durationMs: performance.now() - startTime
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const executedTransitions = [];
|
|
861
|
+
for (const transitionId of path.transitions) {
|
|
862
|
+
const result = await this.executeTransition(transitionId);
|
|
863
|
+
if (!result.success) {
|
|
864
|
+
return {
|
|
865
|
+
success: false,
|
|
866
|
+
path,
|
|
867
|
+
executedTransitions,
|
|
868
|
+
finalActiveStates: this.getActiveStates(),
|
|
869
|
+
error: result.error,
|
|
870
|
+
durationMs: performance.now() - startTime
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
executedTransitions.push(transitionId);
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
success: true,
|
|
877
|
+
path,
|
|
878
|
+
executedTransitions,
|
|
879
|
+
finalActiveStates: this.getActiveStates(),
|
|
880
|
+
durationMs: performance.now() - startTime
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Create a state snapshot
|
|
885
|
+
*/
|
|
886
|
+
createStateSnapshot() {
|
|
887
|
+
return {
|
|
888
|
+
timestamp: Date.now(),
|
|
889
|
+
activeStates: this.getActiveStates(),
|
|
890
|
+
states: this.getAllStates(),
|
|
891
|
+
groups: this.getAllStateGroups(),
|
|
892
|
+
transitions: this.getAllTransitions()
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Create a snapshot of the current state
|
|
897
|
+
*/
|
|
898
|
+
createSnapshot() {
|
|
899
|
+
return {
|
|
900
|
+
timestamp: Date.now(),
|
|
901
|
+
elements: this.getAllElements().map((el) => ({
|
|
902
|
+
id: el.id,
|
|
903
|
+
type: el.type,
|
|
904
|
+
label: el.label,
|
|
905
|
+
identifier: el.getIdentifier(),
|
|
906
|
+
state: el.getState(),
|
|
907
|
+
actions: el.actions,
|
|
908
|
+
customActions: el.customActions ? Object.keys(el.customActions) : void 0
|
|
909
|
+
})),
|
|
910
|
+
components: this.getAllComponents().map((comp) => ({
|
|
911
|
+
id: comp.id,
|
|
912
|
+
name: comp.name,
|
|
913
|
+
description: comp.description,
|
|
914
|
+
actions: comp.actions.map((a) => a.id),
|
|
915
|
+
elementIds: comp.elementIds
|
|
916
|
+
})),
|
|
917
|
+
workflows: this.getAllWorkflows().map((wf) => ({
|
|
918
|
+
id: wf.id,
|
|
919
|
+
name: wf.name,
|
|
920
|
+
description: wf.description,
|
|
921
|
+
stepCount: wf.steps.length
|
|
922
|
+
}))
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Clear all registrations
|
|
927
|
+
*/
|
|
928
|
+
clear() {
|
|
929
|
+
this.elements.clear();
|
|
930
|
+
this.components.clear();
|
|
931
|
+
this.workflows.clear();
|
|
932
|
+
this.eventListeners.clear();
|
|
933
|
+
this.states.clear();
|
|
934
|
+
this.stateGroups.clear();
|
|
935
|
+
this.transitions.clear();
|
|
936
|
+
this.activeStates.clear();
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Get registry statistics
|
|
940
|
+
*/
|
|
941
|
+
getStats() {
|
|
942
|
+
const elements = this.getAllElements();
|
|
943
|
+
const components = this.getAllComponents();
|
|
944
|
+
return {
|
|
945
|
+
elementCount: elements.length,
|
|
946
|
+
componentCount: components.length,
|
|
947
|
+
workflowCount: this.workflows.size,
|
|
948
|
+
mountedElementCount: elements.filter((e) => e.mounted).length,
|
|
949
|
+
mountedComponentCount: components.filter((c) => c.mounted).length,
|
|
950
|
+
stateCount: this.states.size,
|
|
951
|
+
stateGroupCount: this.stateGroups.size,
|
|
952
|
+
transitionCount: this.transitions.size,
|
|
953
|
+
activeStateCount: this.activeStates.size
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
var globalRegistry = null;
|
|
958
|
+
function getGlobalRegistry() {
|
|
959
|
+
if (!globalRegistry) {
|
|
960
|
+
globalRegistry = new UIBridgeRegistry();
|
|
961
|
+
}
|
|
962
|
+
return globalRegistry;
|
|
963
|
+
}
|
|
964
|
+
function setGlobalRegistry(registry) {
|
|
965
|
+
globalRegistry = registry;
|
|
966
|
+
}
|
|
967
|
+
function resetGlobalRegistry() {
|
|
968
|
+
globalRegistry?.clear();
|
|
969
|
+
globalRegistry = null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/core/websocket-client.ts
|
|
973
|
+
function generateId() {
|
|
974
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
975
|
+
}
|
|
976
|
+
var UIBridgeWSClient = class {
|
|
977
|
+
constructor(config) {
|
|
978
|
+
this.ws = null;
|
|
979
|
+
this.state = "disconnected";
|
|
980
|
+
this.clientId = null;
|
|
981
|
+
this.reconnectAttempts = 0;
|
|
982
|
+
this.reconnectTimer = null;
|
|
983
|
+
this.pingTimer = null;
|
|
984
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
985
|
+
// Event listeners
|
|
986
|
+
this.connectionListeners = /* @__PURE__ */ new Set();
|
|
987
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
988
|
+
this.errorListeners = /* @__PURE__ */ new Set();
|
|
989
|
+
// Current subscriptions
|
|
990
|
+
this.subscriptions = {};
|
|
991
|
+
this.config = {
|
|
992
|
+
url: config.url,
|
|
993
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
994
|
+
reconnectDelay: config.reconnectDelay ?? 1e3,
|
|
995
|
+
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
996
|
+
pingInterval: config.pingInterval ?? 3e4,
|
|
997
|
+
connectionTimeout: config.connectionTimeout ?? 1e4
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Get current connection state
|
|
1002
|
+
*/
|
|
1003
|
+
get connectionState() {
|
|
1004
|
+
return this.state;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Get assigned client ID
|
|
1008
|
+
*/
|
|
1009
|
+
get id() {
|
|
1010
|
+
return this.clientId;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Connect to the WebSocket server
|
|
1014
|
+
*/
|
|
1015
|
+
connect() {
|
|
1016
|
+
return new Promise((resolve, reject) => {
|
|
1017
|
+
if (this.ws && this.state === "connected") {
|
|
1018
|
+
resolve();
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
this.setState("connecting");
|
|
1022
|
+
try {
|
|
1023
|
+
this.ws = new WebSocket(this.config.url);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
this.setState("disconnected");
|
|
1026
|
+
reject(error);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const connectionTimeout = setTimeout(() => {
|
|
1030
|
+
if (this.state === "connecting") {
|
|
1031
|
+
this.ws?.close();
|
|
1032
|
+
this.setState("disconnected");
|
|
1033
|
+
reject(new Error("Connection timeout"));
|
|
1034
|
+
}
|
|
1035
|
+
}, this.config.connectionTimeout);
|
|
1036
|
+
this.ws.onopen = () => {
|
|
1037
|
+
clearTimeout(connectionTimeout);
|
|
1038
|
+
};
|
|
1039
|
+
this.ws.onmessage = (event) => {
|
|
1040
|
+
try {
|
|
1041
|
+
const message = JSON.parse(event.data);
|
|
1042
|
+
this.handleMessage(message);
|
|
1043
|
+
if (message.type === "welcome") {
|
|
1044
|
+
clearTimeout(connectionTimeout);
|
|
1045
|
+
this.reconnectAttempts = 0;
|
|
1046
|
+
this.setState("connected");
|
|
1047
|
+
this.startPingInterval();
|
|
1048
|
+
if (this.subscriptions.events?.length || this.subscriptions.elementIds?.length || this.subscriptions.componentIds?.length) {
|
|
1049
|
+
this.subscribe(this.subscriptions);
|
|
1050
|
+
}
|
|
1051
|
+
resolve();
|
|
1052
|
+
}
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
this.ws.onerror = (_event) => {
|
|
1058
|
+
clearTimeout(connectionTimeout);
|
|
1059
|
+
const error = new Error("WebSocket error");
|
|
1060
|
+
this.notifyError(error);
|
|
1061
|
+
if (this.state === "connecting") {
|
|
1062
|
+
reject(error);
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
this.ws.onclose = () => {
|
|
1066
|
+
clearTimeout(connectionTimeout);
|
|
1067
|
+
this.stopPingInterval();
|
|
1068
|
+
this.clientId = null;
|
|
1069
|
+
const wasConnected = this.state === "connected";
|
|
1070
|
+
this.setState("disconnected");
|
|
1071
|
+
for (const [_id, pending] of this.pendingRequests) {
|
|
1072
|
+
clearTimeout(pending.timeout);
|
|
1073
|
+
pending.reject(new Error("Connection closed"));
|
|
1074
|
+
}
|
|
1075
|
+
this.pendingRequests.clear();
|
|
1076
|
+
if (wasConnected && this.config.autoReconnect && (this.config.maxReconnectAttempts === 0 || this.reconnectAttempts < this.config.maxReconnectAttempts)) {
|
|
1077
|
+
this.scheduleReconnect();
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Disconnect from the server
|
|
1084
|
+
*/
|
|
1085
|
+
disconnect() {
|
|
1086
|
+
if (this.reconnectTimer) {
|
|
1087
|
+
clearTimeout(this.reconnectTimer);
|
|
1088
|
+
this.reconnectTimer = null;
|
|
1089
|
+
}
|
|
1090
|
+
this.stopPingInterval();
|
|
1091
|
+
if (this.ws) {
|
|
1092
|
+
this.ws.close();
|
|
1093
|
+
this.ws = null;
|
|
1094
|
+
}
|
|
1095
|
+
this.setState("disconnected");
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Subscribe to events
|
|
1099
|
+
*/
|
|
1100
|
+
async subscribe(options) {
|
|
1101
|
+
this.subscriptions = { ...this.subscriptions, ...options };
|
|
1102
|
+
const response = await this.sendRequest({
|
|
1103
|
+
id: generateId(),
|
|
1104
|
+
type: "subscribe",
|
|
1105
|
+
timestamp: Date.now(),
|
|
1106
|
+
payload: options
|
|
1107
|
+
});
|
|
1108
|
+
return response.events;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Unsubscribe from events
|
|
1112
|
+
*/
|
|
1113
|
+
async unsubscribe(events) {
|
|
1114
|
+
if (events) {
|
|
1115
|
+
this.subscriptions.events = this.subscriptions.events?.filter((e) => !events.includes(e));
|
|
1116
|
+
} else {
|
|
1117
|
+
this.subscriptions = {};
|
|
1118
|
+
}
|
|
1119
|
+
const response = await this.sendRequest({
|
|
1120
|
+
id: generateId(),
|
|
1121
|
+
type: "unsubscribe",
|
|
1122
|
+
timestamp: Date.now(),
|
|
1123
|
+
payload: { events }
|
|
1124
|
+
});
|
|
1125
|
+
return response.events;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Find elements
|
|
1129
|
+
*/
|
|
1130
|
+
async find(options) {
|
|
1131
|
+
const response = await this.sendRequest({
|
|
1132
|
+
id: generateId(),
|
|
1133
|
+
type: "find",
|
|
1134
|
+
timestamp: Date.now(),
|
|
1135
|
+
payload: options
|
|
1136
|
+
});
|
|
1137
|
+
return response.elements;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Discover elements
|
|
1141
|
+
* @deprecated Use find() instead
|
|
1142
|
+
*/
|
|
1143
|
+
async discover(options) {
|
|
1144
|
+
return this.find(options);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Get element details
|
|
1148
|
+
*/
|
|
1149
|
+
async getElement(elementId, includeState = true) {
|
|
1150
|
+
const response = await this.sendRequest({
|
|
1151
|
+
id: generateId(),
|
|
1152
|
+
type: "getElement",
|
|
1153
|
+
timestamp: Date.now(),
|
|
1154
|
+
payload: { elementId, includeState }
|
|
1155
|
+
});
|
|
1156
|
+
return response.element;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Get full snapshot
|
|
1160
|
+
*/
|
|
1161
|
+
async getSnapshot() {
|
|
1162
|
+
const response = await this.sendRequest({
|
|
1163
|
+
id: generateId(),
|
|
1164
|
+
type: "getSnapshot",
|
|
1165
|
+
timestamp: Date.now()
|
|
1166
|
+
});
|
|
1167
|
+
return response;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Execute action on an element
|
|
1171
|
+
*/
|
|
1172
|
+
async executeAction(elementId, action) {
|
|
1173
|
+
const response = await this.sendRequest({
|
|
1174
|
+
id: generateId(),
|
|
1175
|
+
type: "executeAction",
|
|
1176
|
+
timestamp: Date.now(),
|
|
1177
|
+
payload: { elementId, action }
|
|
1178
|
+
});
|
|
1179
|
+
return response;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Execute component action
|
|
1183
|
+
*/
|
|
1184
|
+
async executeComponentAction(componentId, action, params) {
|
|
1185
|
+
const response = await this.sendRequest({
|
|
1186
|
+
id: generateId(),
|
|
1187
|
+
type: "executeComponentAction",
|
|
1188
|
+
timestamp: Date.now(),
|
|
1189
|
+
payload: { componentId, action, params }
|
|
1190
|
+
});
|
|
1191
|
+
return response;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Execute workflow with optional progress streaming
|
|
1195
|
+
*/
|
|
1196
|
+
async executeWorkflow(workflowId, params, onProgress) {
|
|
1197
|
+
const id = generateId();
|
|
1198
|
+
const progressHandler = onProgress ? (message) => {
|
|
1199
|
+
if (message.type === "workflowProgress" && message.requestId === id) {
|
|
1200
|
+
onProgress({
|
|
1201
|
+
currentStep: message.payload.currentStep,
|
|
1202
|
+
totalSteps: message.payload.totalSteps,
|
|
1203
|
+
step: {
|
|
1204
|
+
id: message.payload.step.id,
|
|
1205
|
+
status: message.payload.step.status
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
} : void 0;
|
|
1210
|
+
const response = await this.sendRequest(
|
|
1211
|
+
{
|
|
1212
|
+
id,
|
|
1213
|
+
type: "executeWorkflow",
|
|
1214
|
+
timestamp: Date.now(),
|
|
1215
|
+
payload: { workflowId, params, streamProgress: !!onProgress }
|
|
1216
|
+
},
|
|
1217
|
+
progressHandler
|
|
1218
|
+
);
|
|
1219
|
+
return response;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Add connection state listener
|
|
1223
|
+
*/
|
|
1224
|
+
onConnectionChange(listener) {
|
|
1225
|
+
this.connectionListeners.add(listener);
|
|
1226
|
+
return () => this.connectionListeners.delete(listener);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Add event listener
|
|
1230
|
+
*/
|
|
1231
|
+
onEvent(eventType, listener) {
|
|
1232
|
+
if (!this.eventListeners.has(eventType)) {
|
|
1233
|
+
this.eventListeners.set(eventType, /* @__PURE__ */ new Set());
|
|
1234
|
+
}
|
|
1235
|
+
this.eventListeners.get(eventType).add(listener);
|
|
1236
|
+
return () => this.eventListeners.get(eventType)?.delete(listener);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Add error listener
|
|
1240
|
+
*/
|
|
1241
|
+
onError(listener) {
|
|
1242
|
+
this.errorListeners.add(listener);
|
|
1243
|
+
return () => this.errorListeners.delete(listener);
|
|
1244
|
+
}
|
|
1245
|
+
// Private methods
|
|
1246
|
+
setState(state) {
|
|
1247
|
+
this.state = state;
|
|
1248
|
+
for (const listener of this.connectionListeners) {
|
|
1249
|
+
try {
|
|
1250
|
+
listener(state);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
console.error("Connection listener error:", error);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
handleMessage(message) {
|
|
1257
|
+
switch (message.type) {
|
|
1258
|
+
case "welcome":
|
|
1259
|
+
this.clientId = message.payload.clientId;
|
|
1260
|
+
break;
|
|
1261
|
+
case "pong":
|
|
1262
|
+
break;
|
|
1263
|
+
case "subscribed":
|
|
1264
|
+
case "unsubscribed":
|
|
1265
|
+
break;
|
|
1266
|
+
case "event":
|
|
1267
|
+
this.notifyEvent(message.payload);
|
|
1268
|
+
break;
|
|
1269
|
+
case "response":
|
|
1270
|
+
this.handleResponse(message);
|
|
1271
|
+
break;
|
|
1272
|
+
case "error":
|
|
1273
|
+
if (message.requestId) {
|
|
1274
|
+
this.handleResponse({
|
|
1275
|
+
...message,
|
|
1276
|
+
type: "response",
|
|
1277
|
+
requestId: message.requestId,
|
|
1278
|
+
payload: {
|
|
1279
|
+
success: false,
|
|
1280
|
+
error: message.payload.message
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
} else {
|
|
1284
|
+
this.notifyError(new Error(message.payload.message));
|
|
1285
|
+
}
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
handleResponse(message) {
|
|
1290
|
+
const pending = this.pendingRequests.get(message.requestId);
|
|
1291
|
+
if (!pending) return;
|
|
1292
|
+
clearTimeout(pending.timeout);
|
|
1293
|
+
this.pendingRequests.delete(message.requestId);
|
|
1294
|
+
if (message.type === "response") {
|
|
1295
|
+
if (message.payload.success) {
|
|
1296
|
+
pending.resolve(message.payload.data);
|
|
1297
|
+
} else {
|
|
1298
|
+
pending.reject(new Error(message.payload.error || "Request failed"));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
notifyEvent(event) {
|
|
1303
|
+
const typeListeners = this.eventListeners.get(event.type);
|
|
1304
|
+
if (typeListeners) {
|
|
1305
|
+
for (const listener of typeListeners) {
|
|
1306
|
+
try {
|
|
1307
|
+
listener(event);
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
console.error("Event listener error:", error);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const wildcardListeners = this.eventListeners.get("*");
|
|
1314
|
+
if (wildcardListeners) {
|
|
1315
|
+
for (const listener of wildcardListeners) {
|
|
1316
|
+
try {
|
|
1317
|
+
listener(event);
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
console.error("Event listener error:", error);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
notifyError(error) {
|
|
1325
|
+
for (const listener of this.errorListeners) {
|
|
1326
|
+
try {
|
|
1327
|
+
listener(error);
|
|
1328
|
+
} catch (e) {
|
|
1329
|
+
console.error("Error listener error:", e);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
sendRequest(message, progressHandler) {
|
|
1334
|
+
return new Promise((resolve, reject) => {
|
|
1335
|
+
if (!this.ws || this.state !== "connected") {
|
|
1336
|
+
reject(new Error("Not connected"));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const timeout = setTimeout(() => {
|
|
1340
|
+
this.pendingRequests.delete(message.id);
|
|
1341
|
+
reject(new Error("Request timeout"));
|
|
1342
|
+
}, 3e4);
|
|
1343
|
+
this.pendingRequests.set(message.id, {
|
|
1344
|
+
resolve,
|
|
1345
|
+
reject,
|
|
1346
|
+
timeout
|
|
1347
|
+
});
|
|
1348
|
+
if (progressHandler && this.ws) {
|
|
1349
|
+
const originalHandler = this.ws.onmessage;
|
|
1350
|
+
const wsRef = this.ws;
|
|
1351
|
+
const wrappedHandler = (event) => {
|
|
1352
|
+
try {
|
|
1353
|
+
const msg = JSON.parse(event.data);
|
|
1354
|
+
if (msg.type === "workflowProgress") {
|
|
1355
|
+
progressHandler(msg);
|
|
1356
|
+
}
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
if (originalHandler) {
|
|
1360
|
+
originalHandler.call(wsRef, event);
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
this.ws.onmessage = wrappedHandler;
|
|
1364
|
+
}
|
|
1365
|
+
this.ws.send(JSON.stringify(message));
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
scheduleReconnect() {
|
|
1369
|
+
if (this.reconnectTimer) return;
|
|
1370
|
+
this.setState("reconnecting");
|
|
1371
|
+
this.reconnectAttempts++;
|
|
1372
|
+
const delay = Math.min(
|
|
1373
|
+
this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
1374
|
+
3e4
|
|
1375
|
+
);
|
|
1376
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1377
|
+
this.reconnectTimer = null;
|
|
1378
|
+
this.connect().catch(() => {
|
|
1379
|
+
});
|
|
1380
|
+
}, delay);
|
|
1381
|
+
}
|
|
1382
|
+
startPingInterval() {
|
|
1383
|
+
if (this.config.pingInterval <= 0) return;
|
|
1384
|
+
this.pingTimer = setInterval(() => {
|
|
1385
|
+
if (this.ws && this.state === "connected") {
|
|
1386
|
+
this.ws.send(
|
|
1387
|
+
JSON.stringify({
|
|
1388
|
+
id: generateId(),
|
|
1389
|
+
type: "ping",
|
|
1390
|
+
timestamp: Date.now()
|
|
1391
|
+
})
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}, this.config.pingInterval);
|
|
1395
|
+
}
|
|
1396
|
+
stopPingInterval() {
|
|
1397
|
+
if (this.pingTimer) {
|
|
1398
|
+
clearInterval(this.pingTimer);
|
|
1399
|
+
this.pingTimer = null;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
function createWSClient(config) {
|
|
1404
|
+
return new UIBridgeWSClient(config);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
export { ID_ATTRIBUTES, UIBridgeRegistry, UIBridgeWSClient, createElementIdentifier, createWSClient, elementMatchesIdentifier, findAllElementsByIdentifier, findElementByIdentifier, generateCSSSelector, generateXPath, getBestIdentifier, getGlobalRegistry, resetGlobalRegistry, setGlobalRegistry };
|
|
1408
|
+
//# sourceMappingURL=index.mjs.map
|
|
1409
|
+
//# sourceMappingURL=index.mjs.map
|