@ukiahinsure/a2ui-react-adapter 0.1.0
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 +44 -0
- package/dist/a2ui-structural.css +205 -0
- package/dist/chunk-AOBRTDAU.js +33 -0
- package/dist/chunk-AOBRTDAU.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +2854 -0
- package/dist/index.js.map +1 -0
- package/dist/providerValidation.d.ts +59 -0
- package/dist/providerValidation.js +249 -0
- package/dist/providerValidation.js.map +1 -0
- package/dist/session-B_2-bK2s.d.ts +348 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2854 @@
|
|
|
1
|
+
import {
|
|
2
|
+
A2UI_CAPABILITIES_TOPIC,
|
|
3
|
+
A2UI_CLIENT_TOPIC,
|
|
4
|
+
A2UI_PROTOCOL_VERSION,
|
|
5
|
+
A2UI_SERVER_TOPIC,
|
|
6
|
+
CLIENT_ACTIONS,
|
|
7
|
+
CLIENT_ERROR_CODES,
|
|
8
|
+
ENVELOPE_VERSION,
|
|
9
|
+
MAX_ENVELOPE_BYTES
|
|
10
|
+
} from "./chunk-AOBRTDAU.js";
|
|
11
|
+
|
|
12
|
+
// src/adapter.ts
|
|
13
|
+
import { basicCatalog } from "@a2ui/react/v0_9";
|
|
14
|
+
import {
|
|
15
|
+
A2uiError,
|
|
16
|
+
A2uiMessageListSchema,
|
|
17
|
+
MessageProcessor
|
|
18
|
+
} from "@a2ui/web_core/v0_9";
|
|
19
|
+
var RENDERER_NAME = "a2ui_react_adapter";
|
|
20
|
+
var RENDERER_VERSION = "0.1.0";
|
|
21
|
+
var A2UIClientAdapter = class {
|
|
22
|
+
rendererName = RENDERER_NAME;
|
|
23
|
+
rendererVersion = RENDERER_VERSION;
|
|
24
|
+
#processor;
|
|
25
|
+
#options;
|
|
26
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
27
|
+
#actionListeners = /* @__PURE__ */ new Set();
|
|
28
|
+
#errorListeners = /* @__PURE__ */ new Set();
|
|
29
|
+
#surfaceOrder = [];
|
|
30
|
+
#structureBySurface = /* @__PURE__ */ new Map();
|
|
31
|
+
#version = 0;
|
|
32
|
+
#lastSeq = 0;
|
|
33
|
+
#disposed = false;
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
this.#options = options;
|
|
36
|
+
this.#processor = this.#createProcessor();
|
|
37
|
+
}
|
|
38
|
+
/** Catalog ids this adapter will announce in capability envelopes. */
|
|
39
|
+
get supportedCatalogIds() {
|
|
40
|
+
return [basicCatalog.id];
|
|
41
|
+
}
|
|
42
|
+
get lastSeq() {
|
|
43
|
+
return this.#lastSeq;
|
|
44
|
+
}
|
|
45
|
+
/** The most recently created live surface, if any. */
|
|
46
|
+
get currentSurface() {
|
|
47
|
+
const id = this.#surfaceOrder[this.#surfaceOrder.length - 1];
|
|
48
|
+
return id === void 0 ? void 0 : this.getSurface(id);
|
|
49
|
+
}
|
|
50
|
+
getSurface(id) {
|
|
51
|
+
return this.#processor.model.getSurface(id);
|
|
52
|
+
}
|
|
53
|
+
/** Immutable value-free structure for host accessibility augmentation. */
|
|
54
|
+
getSurfaceStructure(id) {
|
|
55
|
+
const structure = this.#structureBySurface.get(id);
|
|
56
|
+
if (structure === void 0) {
|
|
57
|
+
return { fields: [], tabs: [] };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
fields: [...structure.fieldsByComponent.values()].map((field) => ({
|
|
61
|
+
...field
|
|
62
|
+
})),
|
|
63
|
+
tabs: [...structure.tabsByComponent.values()].flat().map((tab) => ({ ...tab }))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Read a complete scalar field set without exposing the SDK data model. */
|
|
67
|
+
getFieldValues(surfaceId, fields) {
|
|
68
|
+
const surface = this.#processor.model.getSurface(surfaceId);
|
|
69
|
+
if (surface === void 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const values = {};
|
|
73
|
+
for (const field of fields) {
|
|
74
|
+
const value = surface.dataModel.get(fieldPointer(field));
|
|
75
|
+
if (!isFieldValue(value)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
values[field] = value;
|
|
79
|
+
}
|
|
80
|
+
return values;
|
|
81
|
+
}
|
|
82
|
+
/** Restore a private draft after a trusted processor replacement. */
|
|
83
|
+
applyFieldValues(surfaceId, values) {
|
|
84
|
+
const surface = this.#processor.model.getSurface(surfaceId);
|
|
85
|
+
if (surface === void 0) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
let changed = false;
|
|
89
|
+
for (const [field, value] of Object.entries(values)) {
|
|
90
|
+
const path = fieldPointer(field);
|
|
91
|
+
if (!Object.is(surface.dataModel.get(path), value)) {
|
|
92
|
+
surface.dataModel.set(path, value);
|
|
93
|
+
changed = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (changed) {
|
|
97
|
+
this.#notify();
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Apply one validated server envelope. Returns false for duplicates and
|
|
103
|
+
* out-of-order deliveries (the per-session `seq` guard) and for envelopes
|
|
104
|
+
* that fail to apply; committed surfaces are never partially replaced.
|
|
105
|
+
*/
|
|
106
|
+
applyServerEnvelope(envelope) {
|
|
107
|
+
if (this.#disposed) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (envelope.seq <= this.#lastSeq) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
this.#lastSeq = envelope.seq;
|
|
114
|
+
if (envelope.kind === "ack") {
|
|
115
|
+
this.#options.onAck?.(envelope);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return this.#applySurfaceEnvelope(envelope);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reset the ordering guard; used when a reconnect snapshot restarts the
|
|
122
|
+
* server sequence (Queue-Item 024-004).
|
|
123
|
+
*/
|
|
124
|
+
resetSequence() {
|
|
125
|
+
this.#lastSeq = 0;
|
|
126
|
+
}
|
|
127
|
+
/** Clear every generated surface without disposing the reusable adapter. */
|
|
128
|
+
clearSurfaces() {
|
|
129
|
+
if (this.#disposed) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const replacement = this.#createProcessor();
|
|
133
|
+
const previous = this.#processor;
|
|
134
|
+
this.#processor = replacement;
|
|
135
|
+
this.#surfaceOrder = [];
|
|
136
|
+
this.#structureBySurface.clear();
|
|
137
|
+
previous.model.dispose();
|
|
138
|
+
this.#notify();
|
|
139
|
+
}
|
|
140
|
+
/** Emit one of the locked, body-free client error events. */
|
|
141
|
+
reportClientError(errorCode, surfaceId, detail) {
|
|
142
|
+
if (this.#disposed) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const event = { errorCode, surfaceId, detail };
|
|
146
|
+
this.#options.onClientError?.(event);
|
|
147
|
+
for (const listener of this.#errorListeners) {
|
|
148
|
+
listener(event);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Report a rendering failure (used by the surface host error boundary). */
|
|
152
|
+
reportRenderFailure(surfaceId, detail) {
|
|
153
|
+
this.reportClientError("render_failed", surfaceId, detail);
|
|
154
|
+
}
|
|
155
|
+
subscribe = (listener) => {
|
|
156
|
+
this.#listeners.add(listener);
|
|
157
|
+
return () => this.#listeners.delete(listener);
|
|
158
|
+
};
|
|
159
|
+
subscribeActions = (listener) => {
|
|
160
|
+
this.#actionListeners.add(listener);
|
|
161
|
+
return () => this.#actionListeners.delete(listener);
|
|
162
|
+
};
|
|
163
|
+
subscribeClientErrors = (listener) => {
|
|
164
|
+
this.#errorListeners.add(listener);
|
|
165
|
+
return () => this.#errorListeners.delete(listener);
|
|
166
|
+
};
|
|
167
|
+
/** Monotonic change counter for `useSyncExternalStore`. */
|
|
168
|
+
getVersion = () => this.#version;
|
|
169
|
+
dispose() {
|
|
170
|
+
if (this.#disposed) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.#disposed = true;
|
|
174
|
+
this.#processor.model.dispose();
|
|
175
|
+
this.#structureBySurface.clear();
|
|
176
|
+
this.#listeners.clear();
|
|
177
|
+
this.#actionListeners.clear();
|
|
178
|
+
this.#errorListeners.clear();
|
|
179
|
+
}
|
|
180
|
+
#applySurfaceEnvelope(envelope) {
|
|
181
|
+
const validated = A2uiMessageListSchema.safeParse(envelope.messages);
|
|
182
|
+
if (!validated.success) {
|
|
183
|
+
this.reportClientError(
|
|
184
|
+
"message_malformed",
|
|
185
|
+
envelope.surfaceId,
|
|
186
|
+
"message_schema_invalid"
|
|
187
|
+
);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const candidate = this.#createProcessor();
|
|
191
|
+
try {
|
|
192
|
+
if (envelope.kind !== "snapshot") {
|
|
193
|
+
candidate.processMessages(this.#snapshotMessages());
|
|
194
|
+
}
|
|
195
|
+
candidate.processMessages(validated.data);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
candidate.model.dispose();
|
|
198
|
+
this.reportClientError(
|
|
199
|
+
classifyProcessingError(error),
|
|
200
|
+
envelope.surfaceId,
|
|
201
|
+
error instanceof A2uiError ? error.code : "processing_failed"
|
|
202
|
+
);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const previous = this.#processor;
|
|
206
|
+
const nextStructure = updateSurfaceStructures(
|
|
207
|
+
validated.data,
|
|
208
|
+
envelope.kind === "snapshot" ? /* @__PURE__ */ new Map() : cloneSurfaceStructures(this.#structureBySurface)
|
|
209
|
+
);
|
|
210
|
+
pruneSurfaceStructures(nextStructure, candidate);
|
|
211
|
+
if (!fieldInteractionBindingsTrusted(
|
|
212
|
+
envelope.fieldInteractions ?? [],
|
|
213
|
+
nextStructure.get(envelope.surfaceId)
|
|
214
|
+
)) {
|
|
215
|
+
candidate.model.dispose();
|
|
216
|
+
this.reportClientError(
|
|
217
|
+
"message_malformed",
|
|
218
|
+
envelope.surfaceId,
|
|
219
|
+
"field_interaction_untrusted"
|
|
220
|
+
);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
this.#processor = candidate;
|
|
224
|
+
this.#surfaceOrder = [...candidate.model.surfacesMap.keys()];
|
|
225
|
+
this.#structureBySurface = nextStructure;
|
|
226
|
+
previous.model.dispose();
|
|
227
|
+
this.#notify();
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
#createProcessor() {
|
|
231
|
+
return new MessageProcessor(
|
|
232
|
+
[basicCatalog],
|
|
233
|
+
(action) => this.#emitAction(action)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
#emitAction(action) {
|
|
237
|
+
if (!isRecord(action) || !isNonEmptyString(action.name) || !isNonEmptyString(action.surfaceId) || !isNonEmptyString(action.sourceComponentId) || !isRecord(action.context)) {
|
|
238
|
+
this.reportClientError(
|
|
239
|
+
"message_malformed",
|
|
240
|
+
void 0,
|
|
241
|
+
"renderer_action_invalid"
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const event = {
|
|
246
|
+
actionName: action.name,
|
|
247
|
+
surfaceId: action.surfaceId,
|
|
248
|
+
sourceComponentId: action.sourceComponentId,
|
|
249
|
+
context: { ...action.context }
|
|
250
|
+
};
|
|
251
|
+
this.#options.onAction?.(event);
|
|
252
|
+
for (const listener of this.#actionListeners) {
|
|
253
|
+
listener(event);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
#snapshotMessages() {
|
|
257
|
+
const messages = [];
|
|
258
|
+
for (const surface of this.#processor.model.surfacesMap.values()) {
|
|
259
|
+
messages.push({
|
|
260
|
+
version: "v0.9",
|
|
261
|
+
createSurface: {
|
|
262
|
+
surfaceId: surface.id,
|
|
263
|
+
catalogId: surface.catalog.id,
|
|
264
|
+
theme: surface.theme,
|
|
265
|
+
sendDataModel: surface.sendDataModel
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
const components = [...surface.componentsModel.entries].map(
|
|
269
|
+
([id, component]) => ({
|
|
270
|
+
id,
|
|
271
|
+
component: component.type,
|
|
272
|
+
...component.properties
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
if (components.length > 0) {
|
|
276
|
+
messages.push({
|
|
277
|
+
version: "v0.9",
|
|
278
|
+
updateComponents: { surfaceId: surface.id, components }
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const data = surface.dataModel.get("/");
|
|
282
|
+
if (data !== void 0) {
|
|
283
|
+
messages.push({
|
|
284
|
+
version: "v0.9",
|
|
285
|
+
updateDataModel: { surfaceId: surface.id, path: "/", value: data }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return messages;
|
|
290
|
+
}
|
|
291
|
+
#notify() {
|
|
292
|
+
this.#version += 1;
|
|
293
|
+
for (const listener of this.#listeners) {
|
|
294
|
+
listener();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function emptySurfaceStructure() {
|
|
299
|
+
return {
|
|
300
|
+
fieldsByComponent: /* @__PURE__ */ new Map(),
|
|
301
|
+
tabsByComponent: /* @__PURE__ */ new Map()
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function cloneSurfaceStructures(source) {
|
|
305
|
+
return new Map(
|
|
306
|
+
[...source].map(([surfaceId, structure]) => [
|
|
307
|
+
surfaceId,
|
|
308
|
+
{
|
|
309
|
+
fieldsByComponent: new Map(
|
|
310
|
+
[...structure.fieldsByComponent].map(([id, field]) => [
|
|
311
|
+
id,
|
|
312
|
+
{ ...field }
|
|
313
|
+
])
|
|
314
|
+
),
|
|
315
|
+
tabsByComponent: new Map(
|
|
316
|
+
[...structure.tabsByComponent].map(([id, tabs]) => [
|
|
317
|
+
id,
|
|
318
|
+
tabs.map((tab) => ({ ...tab }))
|
|
319
|
+
])
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
])
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
function updateSurfaceStructures(messages, structures) {
|
|
326
|
+
for (const message of messages) {
|
|
327
|
+
if (!isRecord(message)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const deletion = message.deleteSurface;
|
|
331
|
+
if (isRecord(deletion) && isNonEmptyString(deletion.surfaceId)) {
|
|
332
|
+
structures.delete(deletion.surfaceId);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const creation = message.createSurface;
|
|
336
|
+
if (isRecord(creation) && isNonEmptyString(creation.surfaceId)) {
|
|
337
|
+
structures.set(creation.surfaceId, emptySurfaceStructure());
|
|
338
|
+
}
|
|
339
|
+
const update = message.updateComponents;
|
|
340
|
+
if (!isRecord(update) || !isNonEmptyString(update.surfaceId) || !Array.isArray(update.components)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const structure = structures.get(update.surfaceId) ?? emptySurfaceStructure();
|
|
344
|
+
structures.set(update.surfaceId, structure);
|
|
345
|
+
for (const component of update.components) {
|
|
346
|
+
if (!isRecord(component) || !isNonEmptyString(component.id)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
structure.fieldsByComponent.delete(component.id);
|
|
350
|
+
structure.tabsByComponent.delete(component.id);
|
|
351
|
+
const field = fieldDescriptor(component);
|
|
352
|
+
if (field !== void 0) {
|
|
353
|
+
structure.fieldsByComponent.set(component.id, field);
|
|
354
|
+
}
|
|
355
|
+
const tabs = tabDescriptors(component);
|
|
356
|
+
if (tabs.length > 0) {
|
|
357
|
+
structure.tabsByComponent.set(component.id, tabs);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return structures;
|
|
362
|
+
}
|
|
363
|
+
function pruneSurfaceStructures(structures, processor) {
|
|
364
|
+
for (const [surfaceId, structure] of structures) {
|
|
365
|
+
const surface = processor.model.getSurface(surfaceId);
|
|
366
|
+
if (surface === void 0) {
|
|
367
|
+
structures.delete(surfaceId);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const componentIds = new Set(
|
|
371
|
+
[...surface.componentsModel.entries].map(([componentId]) => componentId)
|
|
372
|
+
);
|
|
373
|
+
for (const componentId of structure.fieldsByComponent.keys()) {
|
|
374
|
+
if (!componentIds.has(componentId)) {
|
|
375
|
+
structure.fieldsByComponent.delete(componentId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const componentId of structure.tabsByComponent.keys()) {
|
|
379
|
+
if (!componentIds.has(componentId)) {
|
|
380
|
+
structure.tabsByComponent.delete(componentId);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function fieldInteractionBindingsTrusted(groups, structure) {
|
|
386
|
+
if (groups.length === 0) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
if (structure === void 0) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
return groups.every(
|
|
393
|
+
(group) => group.fields.every((binding) => {
|
|
394
|
+
const field = structure.fieldsByComponent.get(binding.componentId);
|
|
395
|
+
return field?.field === binding.field;
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
function fieldDescriptor(component) {
|
|
400
|
+
const controls = {
|
|
401
|
+
TextField: "text",
|
|
402
|
+
DateTimeInput: "date",
|
|
403
|
+
ChoicePicker: "choice"
|
|
404
|
+
};
|
|
405
|
+
const control = typeof component.component === "string" ? controls[component.component] : void 0;
|
|
406
|
+
const value = component.value;
|
|
407
|
+
const field = isRecord(value) && typeof value.path === "string" ? fieldFromPointer(value.path) : void 0;
|
|
408
|
+
if (control === void 0 || field === void 0 || !isNonEmptyString(component.id)) {
|
|
409
|
+
return void 0;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
componentId: component.id,
|
|
413
|
+
field,
|
|
414
|
+
label: typeof component.label === "string" && component.label.length > 0 ? component.label : field,
|
|
415
|
+
control,
|
|
416
|
+
required: hasRequiredCheck(component.checks)
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function hasRequiredCheck(value) {
|
|
420
|
+
return Array.isArray(value) && value.some(
|
|
421
|
+
(check) => isRecord(check) && isRecord(check.condition) && check.condition.call === "required"
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
function fieldFromPointer(path) {
|
|
425
|
+
if (!path.startsWith("/") || path.length < 2 || path.slice(1).includes("/")) {
|
|
426
|
+
return void 0;
|
|
427
|
+
}
|
|
428
|
+
return path.slice(1).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
429
|
+
}
|
|
430
|
+
function fieldPointer(field) {
|
|
431
|
+
return `/${field.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
432
|
+
}
|
|
433
|
+
function isFieldValue(value) {
|
|
434
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
435
|
+
}
|
|
436
|
+
function tabDescriptors(component) {
|
|
437
|
+
if (component.component !== "Tabs" || !isNonEmptyString(component.id) || !Array.isArray(component.tabs)) {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
const tabs = [];
|
|
441
|
+
for (const tab of component.tabs) {
|
|
442
|
+
if (isRecord(tab) && isNonEmptyString(tab.title) && isNonEmptyString(tab.child)) {
|
|
443
|
+
tabs.push({
|
|
444
|
+
componentId: component.id,
|
|
445
|
+
title: tab.title,
|
|
446
|
+
childId: tab.child
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return tabs;
|
|
451
|
+
}
|
|
452
|
+
function isRecord(value) {
|
|
453
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
454
|
+
}
|
|
455
|
+
function isNonEmptyString(value) {
|
|
456
|
+
return typeof value === "string" && value.length > 0;
|
|
457
|
+
}
|
|
458
|
+
function classifyProcessingError(error) {
|
|
459
|
+
if (error instanceof A2uiError && typeof error.message === "string" && error.message.startsWith("Catalog not found")) {
|
|
460
|
+
return "catalog_unsupported";
|
|
461
|
+
}
|
|
462
|
+
return "message_malformed";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/SurfaceHost.tsx
|
|
466
|
+
import {
|
|
467
|
+
Component,
|
|
468
|
+
useId,
|
|
469
|
+
useLayoutEffect,
|
|
470
|
+
useRef,
|
|
471
|
+
useSyncExternalStore
|
|
472
|
+
} from "react";
|
|
473
|
+
import { A2uiSurface } from "@a2ui/react/v0_9";
|
|
474
|
+
import { jsx } from "react/jsx-runtime";
|
|
475
|
+
function A2UISurfaceHost({
|
|
476
|
+
adapter,
|
|
477
|
+
surfaceId,
|
|
478
|
+
surfaceMetadata,
|
|
479
|
+
sessionController,
|
|
480
|
+
hostId,
|
|
481
|
+
ariaLabel = "Current intake step",
|
|
482
|
+
disabled = false,
|
|
483
|
+
busy = false,
|
|
484
|
+
invalidFields = [],
|
|
485
|
+
validationSummaryId,
|
|
486
|
+
noSurfaceFallback,
|
|
487
|
+
errorFallback
|
|
488
|
+
}) {
|
|
489
|
+
const version = useSyncExternalStore(
|
|
490
|
+
adapter.subscribe,
|
|
491
|
+
adapter.getVersion,
|
|
492
|
+
adapter.getVersion
|
|
493
|
+
);
|
|
494
|
+
const readSessionSnapshot = sessionController?.getSnapshot ?? getNoSessionSnapshot;
|
|
495
|
+
const subscribeSession = sessionController?.subscribe ?? subscribeToNothing;
|
|
496
|
+
const controllerSnapshot = useSyncExternalStore(
|
|
497
|
+
subscribeSession,
|
|
498
|
+
readSessionSnapshot,
|
|
499
|
+
readSessionSnapshot
|
|
500
|
+
);
|
|
501
|
+
const generatedId = useId().replaceAll(":", "");
|
|
502
|
+
const rootRef = useRef(null);
|
|
503
|
+
const lastFocusedState = useRef(void 0);
|
|
504
|
+
const lastViewedSection = useRef(void 0);
|
|
505
|
+
const lastValidationKey = useRef("");
|
|
506
|
+
const resolvedSurfaceMetadata = surfaceMetadata ?? controllerSnapshot?.surface ?? null;
|
|
507
|
+
const editing = controllerSnapshot?.editing ?? editingForSurface(resolvedSurfaceMetadata);
|
|
508
|
+
const surface = surfaceId === void 0 ? adapter.currentSurface : adapter.getSurface(surfaceId);
|
|
509
|
+
const structure = surface === void 0 ? EMPTY_STRUCTURE : adapter.getSurfaceStructure(surface.id);
|
|
510
|
+
const focusKey = surface === void 0 ? void 0 : `${surface.id}:${resolvedSurfaceMetadata?.activeStateId ?? "surface"}`;
|
|
511
|
+
const frameId = hostId ?? `a2ui-surface-${generatedId}`;
|
|
512
|
+
useLayoutEffect(() => {
|
|
513
|
+
const root = rootRef.current;
|
|
514
|
+
if (root === null || surface === void 0) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
let cancelled = false;
|
|
518
|
+
let controlCleanups = [];
|
|
519
|
+
const refreshControls = () => {
|
|
520
|
+
if (cancelled || !root.isConnected) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
for (const cleanup of controlCleanups.reverse()) {
|
|
524
|
+
cleanup();
|
|
525
|
+
}
|
|
526
|
+
controlCleanups = [
|
|
527
|
+
annotateFields(
|
|
528
|
+
root,
|
|
529
|
+
structure.fields,
|
|
530
|
+
new Set(invalidFields),
|
|
531
|
+
validationSummaryId
|
|
532
|
+
),
|
|
533
|
+
formatFieldLabels(root),
|
|
534
|
+
bindFieldInteractionGroups(
|
|
535
|
+
root,
|
|
536
|
+
resolvedSurfaceMetadata?.fieldInteractions ?? [],
|
|
537
|
+
editing,
|
|
538
|
+
sessionController
|
|
539
|
+
),
|
|
540
|
+
hideGeneratedGroupLabels(
|
|
541
|
+
root,
|
|
542
|
+
resolvedSurfaceMetadata?.fieldInteractions ?? []
|
|
543
|
+
),
|
|
544
|
+
annotateActionControls(root),
|
|
545
|
+
setPendingGroupsDisabled(
|
|
546
|
+
root,
|
|
547
|
+
new Set(editing.pendingGroupIds)
|
|
548
|
+
),
|
|
549
|
+
setInteractionDisabled(root, disabled || busy)
|
|
550
|
+
];
|
|
551
|
+
};
|
|
552
|
+
const scheduleControlRefresh = () => {
|
|
553
|
+
queueMicrotask(refreshControls);
|
|
554
|
+
};
|
|
555
|
+
const tabCleanup = enhanceTabs(
|
|
556
|
+
root,
|
|
557
|
+
structure.tabs,
|
|
558
|
+
resolvedSurfaceMetadata?.sectionNavigation ?? null,
|
|
559
|
+
editing.viewedSectionId,
|
|
560
|
+
frameId,
|
|
561
|
+
scheduleControlRefresh,
|
|
562
|
+
(sectionId) => sessionController?.viewSection(sectionId)
|
|
563
|
+
);
|
|
564
|
+
scheduleControlRefresh();
|
|
565
|
+
const previousViewedSection = lastViewedSection.current;
|
|
566
|
+
const authoritativeSection = editing.authoritativeSectionId;
|
|
567
|
+
const hasRetainedEdit = editing.dirtyGroupIds.length > 0 || editing.pendingGroupIds.length > 0 || editing.rejectedGroupIds.length > 0;
|
|
568
|
+
const authoritativeStateChanged = focusKey !== void 0 && lastFocusedState.current !== focusKey;
|
|
569
|
+
const returnedToCurrent = previousViewedSection !== void 0 && previousViewedSection !== authoritativeSection && editing.viewedSectionId === authoritativeSection;
|
|
570
|
+
const validationKey = invalidFields.length === 0 ? "" : `${controllerSnapshot?.actions.lastResult?.requestId ?? "fields"}:${invalidFields.join("\0")}`;
|
|
571
|
+
const validationChanged = validationKey.length > 0 && validationKey !== lastValidationKey.current;
|
|
572
|
+
lastFocusedState.current = focusKey;
|
|
573
|
+
lastViewedSection.current = editing.viewedSectionId;
|
|
574
|
+
lastValidationKey.current = validationKey;
|
|
575
|
+
if (validationChanged && validationSummaryId === void 0 || returnedToCurrent || authoritativeStateChanged && !hasRetainedEdit && editing.viewedSectionId === authoritativeSection) {
|
|
576
|
+
queueMicrotask(() => {
|
|
577
|
+
refreshControls();
|
|
578
|
+
if (cancelled || !root.isConnected) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
focusProgressTarget(
|
|
582
|
+
root,
|
|
583
|
+
resolvedSurfaceMetadata,
|
|
584
|
+
invalidFields
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
return () => {
|
|
589
|
+
cancelled = true;
|
|
590
|
+
for (const cleanup of controlCleanups.reverse()) {
|
|
591
|
+
cleanup();
|
|
592
|
+
}
|
|
593
|
+
tabCleanup();
|
|
594
|
+
};
|
|
595
|
+
}, [
|
|
596
|
+
busy,
|
|
597
|
+
disabled,
|
|
598
|
+
editing,
|
|
599
|
+
focusKey,
|
|
600
|
+
frameId,
|
|
601
|
+
invalidFields,
|
|
602
|
+
structure,
|
|
603
|
+
surface,
|
|
604
|
+
resolvedSurfaceMetadata,
|
|
605
|
+
sessionController,
|
|
606
|
+
controllerSnapshot?.actions.lastResult?.requestId,
|
|
607
|
+
validationSummaryId,
|
|
608
|
+
version
|
|
609
|
+
]);
|
|
610
|
+
if (surface === void 0) {
|
|
611
|
+
return noSurfaceFallback ?? /* @__PURE__ */ jsx("div", { "data-a2ui-host": "no-surface", role: "status", "aria-live": "polite", children: "Your conversation continues by voice." });
|
|
612
|
+
}
|
|
613
|
+
return /* @__PURE__ */ jsx(
|
|
614
|
+
"div",
|
|
615
|
+
{
|
|
616
|
+
ref: rootRef,
|
|
617
|
+
id: frameId,
|
|
618
|
+
className: "a2ui-accessible-surface",
|
|
619
|
+
"data-a2ui-host": "surface",
|
|
620
|
+
"data-a2ui-disabled": disabled || busy ? "true" : "false",
|
|
621
|
+
role: "region",
|
|
622
|
+
"aria-label": ariaLabel,
|
|
623
|
+
"aria-busy": busy || void 0,
|
|
624
|
+
"aria-disabled": disabled || busy || void 0,
|
|
625
|
+
tabIndex: -1,
|
|
626
|
+
children: /* @__PURE__ */ jsx(
|
|
627
|
+
SurfaceErrorBoundary,
|
|
628
|
+
{
|
|
629
|
+
adapter,
|
|
630
|
+
surface,
|
|
631
|
+
fallback: errorFallback ?? /* @__PURE__ */ jsx("div", { "data-a2ui-host": "render-error", role: "alert", children: "This step can't be shown right now. Please continue by voice." }),
|
|
632
|
+
children: /* @__PURE__ */ jsx(A2uiSurface, { surface })
|
|
633
|
+
},
|
|
634
|
+
surface.id
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
function getNoSessionSnapshot() {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
function subscribeToNothing() {
|
|
643
|
+
return () => void 0;
|
|
644
|
+
}
|
|
645
|
+
function editingForSurface(surface) {
|
|
646
|
+
const authoritative = surface?.sectionNavigation?.activeSectionId ?? null;
|
|
647
|
+
return {
|
|
648
|
+
authoritativeSectionId: authoritative,
|
|
649
|
+
viewedSectionId: authoritative,
|
|
650
|
+
dirtyGroupIds: [],
|
|
651
|
+
pendingGroupIds: [],
|
|
652
|
+
rejectedGroupIds: []
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function focusProgressTarget(root, metadata, invalidFields) {
|
|
656
|
+
const controls = [
|
|
657
|
+
...root.querySelectorAll("[data-a2ui-field]")
|
|
658
|
+
].filter((control) => !isControlDisabled(control));
|
|
659
|
+
const invalid2 = invalidFields.map(
|
|
660
|
+
(field) => controls.find((control) => control.dataset.a2uiField === field)
|
|
661
|
+
).find((control) => control !== void 0);
|
|
662
|
+
const currentFields = metadata?.fieldInteractions.find((group) => group.mode === "current")?.fields.map((binding) => binding.field) ?? [];
|
|
663
|
+
const current = currentFields.map(
|
|
664
|
+
(field) => controls.find((control) => control.dataset.a2uiField === field)
|
|
665
|
+
).find((control) => control !== void 0);
|
|
666
|
+
const target = invalid2 ?? current ?? controls[0] ?? root;
|
|
667
|
+
target.focus({ preventScroll: true });
|
|
668
|
+
if (target !== root) {
|
|
669
|
+
target.scrollIntoView?.({
|
|
670
|
+
block: "nearest",
|
|
671
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth"
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function isControlDisabled(control) {
|
|
676
|
+
return control.getAttribute("aria-disabled") === "true" || "disabled" in control && control.disabled === true;
|
|
677
|
+
}
|
|
678
|
+
function prefersReducedMotion() {
|
|
679
|
+
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
680
|
+
}
|
|
681
|
+
function bindFieldInteractionGroups(root, groups, editing, controller) {
|
|
682
|
+
if (groups.length === 0) {
|
|
683
|
+
return () => void 0;
|
|
684
|
+
}
|
|
685
|
+
const groupByField = new Map(
|
|
686
|
+
groups.flatMap(
|
|
687
|
+
(group) => group.fields.map(
|
|
688
|
+
(binding) => [binding.field, group]
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
);
|
|
692
|
+
const originalGroupAttributes = /* @__PURE__ */ new Map();
|
|
693
|
+
for (const control of root.querySelectorAll(
|
|
694
|
+
"[data-a2ui-field]"
|
|
695
|
+
)) {
|
|
696
|
+
const field = control.dataset.a2uiField;
|
|
697
|
+
const group = field === void 0 ? void 0 : groupByField.get(field);
|
|
698
|
+
if (group !== void 0) {
|
|
699
|
+
originalGroupAttributes.set(control, {
|
|
700
|
+
groupId: control.dataset.a2uiGroupId,
|
|
701
|
+
groupMode: control.dataset.a2uiGroupMode,
|
|
702
|
+
editState: control.dataset.a2uiEditState
|
|
703
|
+
});
|
|
704
|
+
control.dataset.a2uiGroupId = group.groupId;
|
|
705
|
+
control.dataset.a2uiGroupMode = group.mode;
|
|
706
|
+
control.dataset.a2uiEditState = groupEditState(group.groupId, editing);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (controller === void 0) {
|
|
710
|
+
return () => {
|
|
711
|
+
restoreGroupAttributes(originalGroupAttributes);
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const queuedCaptures = /* @__PURE__ */ new Set();
|
|
715
|
+
const scheduleCapture = (groupId) => {
|
|
716
|
+
if (queuedCaptures.has(groupId)) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
queuedCaptures.add(groupId);
|
|
720
|
+
queueMicrotask(() => {
|
|
721
|
+
queuedCaptures.delete(groupId);
|
|
722
|
+
controller.captureFieldGroupDraft(groupId);
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
const onInput = (event) => {
|
|
726
|
+
const groupId = interactionGroupId(root, event.target);
|
|
727
|
+
if (groupId !== void 0) {
|
|
728
|
+
scheduleCapture(groupId);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
const onFocusOut = (event) => {
|
|
732
|
+
const groupId = interactionGroupId(root, event.target);
|
|
733
|
+
const group = groups.find((candidate) => candidate.groupId === groupId);
|
|
734
|
+
if (groupId === void 0 || group?.mode === "current" || interactionGroupId(root, event.relatedTarget) === groupId) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
queueMicrotask(() => {
|
|
738
|
+
if (interactionGroupId(root, root.ownerDocument.activeElement) !== groupId) {
|
|
739
|
+
controller.commitFieldGroup(groupId);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
root.addEventListener("input", onInput, true);
|
|
744
|
+
root.addEventListener("change", onInput, true);
|
|
745
|
+
root.addEventListener("focusout", onFocusOut, true);
|
|
746
|
+
return () => {
|
|
747
|
+
root.removeEventListener("input", onInput, true);
|
|
748
|
+
root.removeEventListener("change", onInput, true);
|
|
749
|
+
root.removeEventListener("focusout", onFocusOut, true);
|
|
750
|
+
restoreGroupAttributes(originalGroupAttributes);
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function groupEditState(groupId, editing) {
|
|
754
|
+
if (editing.pendingGroupIds.includes(groupId)) {
|
|
755
|
+
return "saving";
|
|
756
|
+
}
|
|
757
|
+
if (editing.rejectedGroupIds.includes(groupId)) {
|
|
758
|
+
return "rejected";
|
|
759
|
+
}
|
|
760
|
+
return editing.dirtyGroupIds.includes(groupId) ? "dirty" : "saved";
|
|
761
|
+
}
|
|
762
|
+
function hideGeneratedGroupLabels(root, groups) {
|
|
763
|
+
if (groups.length === 0) {
|
|
764
|
+
return () => void 0;
|
|
765
|
+
}
|
|
766
|
+
const originals = /* @__PURE__ */ new Map();
|
|
767
|
+
for (const group of groups) {
|
|
768
|
+
const controls = [
|
|
769
|
+
...root.querySelectorAll("[data-a2ui-group-id]")
|
|
770
|
+
].filter((control) => control.dataset.a2uiGroupId === group.groupId);
|
|
771
|
+
const label = generatedGroupLabelForControls(root, controls);
|
|
772
|
+
if (label === void 0 || originals.has(label)) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
originals.set(label, {
|
|
776
|
+
hidden: label.hidden,
|
|
777
|
+
ariaHidden: label.getAttribute("aria-hidden"),
|
|
778
|
+
generatedGroupLabel: label.dataset.a2uiGeneratedGroupLabel
|
|
779
|
+
});
|
|
780
|
+
label.hidden = true;
|
|
781
|
+
label.setAttribute("aria-hidden", "true");
|
|
782
|
+
label.dataset.a2uiGeneratedGroupLabel = "true";
|
|
783
|
+
}
|
|
784
|
+
return () => {
|
|
785
|
+
for (const [label, original] of originals) {
|
|
786
|
+
label.hidden = original.hidden;
|
|
787
|
+
restoreAttribute(label, "aria-hidden", original.ariaHidden);
|
|
788
|
+
restoreDataset(
|
|
789
|
+
label,
|
|
790
|
+
"a2uiGeneratedGroupLabel",
|
|
791
|
+
original.generatedGroupLabel
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function generatedGroupLabelForControls(root, controls) {
|
|
797
|
+
for (const control of controls) {
|
|
798
|
+
const label = generatedGroupLabelForControl(root, control);
|
|
799
|
+
if (label !== void 0) {
|
|
800
|
+
return label;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return void 0;
|
|
804
|
+
}
|
|
805
|
+
function generatedGroupLabelForControl(root, control) {
|
|
806
|
+
let current = control;
|
|
807
|
+
while (current !== null && current !== root) {
|
|
808
|
+
const parent = current.parentElement;
|
|
809
|
+
if (parent === null) {
|
|
810
|
+
return void 0;
|
|
811
|
+
}
|
|
812
|
+
const siblings = [...parent.children];
|
|
813
|
+
const currentIndex = siblings.indexOf(current);
|
|
814
|
+
const precedingText = siblings.slice(0, currentIndex).filter(isGeneratedTextElement);
|
|
815
|
+
if (precedingText.length >= 2) {
|
|
816
|
+
return precedingText[0];
|
|
817
|
+
}
|
|
818
|
+
if (precedingText.length === 1 && subtreeHasGeneratedTextElement(current)) {
|
|
819
|
+
return precedingText[0];
|
|
820
|
+
}
|
|
821
|
+
current = parent;
|
|
822
|
+
}
|
|
823
|
+
return void 0;
|
|
824
|
+
}
|
|
825
|
+
function subtreeHasGeneratedTextElement(element) {
|
|
826
|
+
return [...element.querySelectorAll("*")].some(isGeneratedTextElement);
|
|
827
|
+
}
|
|
828
|
+
function isGeneratedTextElement(element) {
|
|
829
|
+
return element instanceof HTMLElement && element.textContent?.trim() !== "" && element.querySelector("button, input, textarea, select, label") === null && (element.classList.contains("a2ui-text") || element.classList.contains("no-markdown-renderer") || ["h1", "h2", "h3", "h4", "h5", "body", "caption"].some(
|
|
830
|
+
(className) => element.classList.contains(className)
|
|
831
|
+
) || element.querySelector(":scope > section > :is(h1, h2, h3, h4, h5, p)") !== null);
|
|
832
|
+
}
|
|
833
|
+
function restoreGroupAttributes(originals) {
|
|
834
|
+
for (const [control, original] of originals) {
|
|
835
|
+
restoreDataset(control, "a2uiGroupId", original.groupId);
|
|
836
|
+
restoreDataset(control, "a2uiGroupMode", original.groupMode);
|
|
837
|
+
restoreDataset(control, "a2uiEditState", original.editState);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function restoreDataset(element, key, value) {
|
|
841
|
+
if (value === void 0) {
|
|
842
|
+
delete element.dataset[key];
|
|
843
|
+
} else {
|
|
844
|
+
element.dataset[key] = value;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function interactionGroupId(root, target) {
|
|
848
|
+
if (!(target instanceof Element) || !root.contains(target)) {
|
|
849
|
+
return void 0;
|
|
850
|
+
}
|
|
851
|
+
return target.closest("[data-a2ui-group-id]")?.dataset.a2uiGroupId;
|
|
852
|
+
}
|
|
853
|
+
function annotateActionControls(root) {
|
|
854
|
+
const actions = [...root.querySelectorAll("button")].filter(
|
|
855
|
+
(button) => button.getAttribute("role") !== "tab" && ["submit", "skip", "revise"].includes(
|
|
856
|
+
button.textContent?.trim().toLowerCase() ?? ""
|
|
857
|
+
)
|
|
858
|
+
);
|
|
859
|
+
const groups = /* @__PURE__ */ new Set();
|
|
860
|
+
for (const action of actions) {
|
|
861
|
+
action.dataset.a2uiAction = "true";
|
|
862
|
+
if (action.parentElement !== null) {
|
|
863
|
+
action.parentElement.dataset.a2uiActionGroup = "true";
|
|
864
|
+
groups.add(action.parentElement);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return () => {
|
|
868
|
+
for (const action of actions) {
|
|
869
|
+
delete action.dataset.a2uiAction;
|
|
870
|
+
}
|
|
871
|
+
for (const group of groups) {
|
|
872
|
+
delete group.dataset.a2uiActionGroup;
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
var EMPTY_STRUCTURE = { fields: [], tabs: [] };
|
|
877
|
+
var SurfaceErrorBoundary = class extends Component {
|
|
878
|
+
state = { failedSurfaceId: void 0 };
|
|
879
|
+
static getDerivedStateFromError() {
|
|
880
|
+
return { failedSurfaceId: "pending" };
|
|
881
|
+
}
|
|
882
|
+
componentDidCatch(error) {
|
|
883
|
+
this.setState({ failedSurfaceId: this.props.surface.id });
|
|
884
|
+
this.props.adapter.reportRenderFailure(this.props.surface.id, error.name);
|
|
885
|
+
}
|
|
886
|
+
render() {
|
|
887
|
+
if (this.state.failedSurfaceId !== void 0) {
|
|
888
|
+
return /* @__PURE__ */ jsx(
|
|
889
|
+
"div",
|
|
890
|
+
{
|
|
891
|
+
"data-a2ui-error-boundary": "true",
|
|
892
|
+
tabIndex: -1,
|
|
893
|
+
ref: (node) => node?.focus(),
|
|
894
|
+
children: this.props.fallback
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return this.props.children;
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
function annotateFields(root, fields, invalidFields, validationSummaryId) {
|
|
902
|
+
const originalAttributes = /* @__PURE__ */ new Map();
|
|
903
|
+
const matchCleanups = [];
|
|
904
|
+
for (const field of fields) {
|
|
905
|
+
const match = findFieldControls(root, field);
|
|
906
|
+
matchCleanups.push(match.cleanup);
|
|
907
|
+
for (const control of match.controls) {
|
|
908
|
+
if (!originalAttributes.has(control)) {
|
|
909
|
+
originalAttributes.set(control, {
|
|
910
|
+
field: control.dataset.a2uiField,
|
|
911
|
+
required: control.getAttribute("aria-required"),
|
|
912
|
+
invalid: control.getAttribute("aria-invalid"),
|
|
913
|
+
describedBy: control.getAttribute("aria-describedby")
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
control.dataset.a2uiField = field.field;
|
|
917
|
+
if (field.required) {
|
|
918
|
+
control.setAttribute("aria-required", "true");
|
|
919
|
+
}
|
|
920
|
+
if (invalidFields.has(field.field)) {
|
|
921
|
+
control.setAttribute("aria-invalid", "true");
|
|
922
|
+
if (validationSummaryId !== void 0) {
|
|
923
|
+
const describedBy = new Set(
|
|
924
|
+
(control.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean)
|
|
925
|
+
);
|
|
926
|
+
describedBy.add(validationSummaryId);
|
|
927
|
+
control.setAttribute(
|
|
928
|
+
"aria-describedby",
|
|
929
|
+
[...describedBy].join(" ")
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return () => {
|
|
936
|
+
for (const [control, original] of originalAttributes) {
|
|
937
|
+
if (original.field === void 0) {
|
|
938
|
+
delete control.dataset.a2uiField;
|
|
939
|
+
} else {
|
|
940
|
+
control.dataset.a2uiField = original.field;
|
|
941
|
+
}
|
|
942
|
+
restoreAttribute(control, "aria-required", original.required);
|
|
943
|
+
restoreAttribute(control, "aria-invalid", original.invalid);
|
|
944
|
+
restoreAttribute(control, "aria-describedby", original.describedBy);
|
|
945
|
+
}
|
|
946
|
+
for (const cleanup of matchCleanups.reverse()) {
|
|
947
|
+
cleanup();
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function findFieldControls(root, field) {
|
|
952
|
+
const labels = [...root.querySelectorAll("label")];
|
|
953
|
+
const label = labels.find(
|
|
954
|
+
(candidate) => candidate.textContent?.trim() === field.label
|
|
955
|
+
);
|
|
956
|
+
if (label?.htmlFor) {
|
|
957
|
+
const control = root.ownerDocument.getElementById(label.htmlFor);
|
|
958
|
+
if (control instanceof HTMLElement && root.contains(control)) {
|
|
959
|
+
return { controls: [control], cleanup: () => void 0 };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const headings = [...root.querySelectorAll("strong")];
|
|
963
|
+
const heading = headings.find(
|
|
964
|
+
(candidate) => candidate.textContent?.trim() === field.label
|
|
965
|
+
);
|
|
966
|
+
if (heading === void 0) {
|
|
967
|
+
return { controls: [], cleanup: () => void 0 };
|
|
968
|
+
}
|
|
969
|
+
const group = heading.parentElement;
|
|
970
|
+
if (group === null) {
|
|
971
|
+
return { controls: [], cleanup: () => void 0 };
|
|
972
|
+
}
|
|
973
|
+
const originalHeadingId = heading.getAttribute("id");
|
|
974
|
+
const originalRole = group.getAttribute("role");
|
|
975
|
+
const originalLabelledBy = group.getAttribute("aria-labelledby");
|
|
976
|
+
const headingId = heading.id || `${safeDomToken(field.componentId)}-label`;
|
|
977
|
+
heading.id = headingId;
|
|
978
|
+
group.setAttribute("role", "radiogroup");
|
|
979
|
+
group.setAttribute("aria-labelledby", headingId);
|
|
980
|
+
return {
|
|
981
|
+
controls: [
|
|
982
|
+
...group.querySelectorAll(
|
|
983
|
+
'input[type="radio"], input[type="checkbox"], button'
|
|
984
|
+
)
|
|
985
|
+
],
|
|
986
|
+
cleanup: () => {
|
|
987
|
+
restoreAttribute(heading, "id", originalHeadingId);
|
|
988
|
+
restoreAttribute(group, "role", originalRole);
|
|
989
|
+
restoreAttribute(group, "aria-labelledby", originalLabelledBy);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function formatFieldLabels(root) {
|
|
994
|
+
const originals = /* @__PURE__ */ new Map();
|
|
995
|
+
for (const control of root.querySelectorAll("[data-a2ui-field]")) {
|
|
996
|
+
const field = control.dataset.a2uiField;
|
|
997
|
+
if (field === void 0) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const label = labelElementForControl(root, control);
|
|
1001
|
+
if (label === void 0 || originals.has(label)) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
originals.set(label, label.textContent ?? "");
|
|
1005
|
+
label.textContent = displayFieldLabel(field);
|
|
1006
|
+
}
|
|
1007
|
+
return () => {
|
|
1008
|
+
for (const [label, text] of originals) {
|
|
1009
|
+
label.textContent = text;
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function labelElementForControl(root, control) {
|
|
1014
|
+
if (control.id.length > 0) {
|
|
1015
|
+
const label2 = root.querySelector(
|
|
1016
|
+
`label[for="${cssString(control.id)}"]`
|
|
1017
|
+
);
|
|
1018
|
+
if (label2 !== null) {
|
|
1019
|
+
return label2;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const group = control.closest('[role="radiogroup"]');
|
|
1023
|
+
const labelledBy = group?.getAttribute("aria-labelledby");
|
|
1024
|
+
if (labelledBy === null || labelledBy === void 0 || labelledBy === "") {
|
|
1025
|
+
return void 0;
|
|
1026
|
+
}
|
|
1027
|
+
const label = root.ownerDocument.getElementById(labelledBy);
|
|
1028
|
+
return label instanceof HTMLElement && root.contains(label) ? label : void 0;
|
|
1029
|
+
}
|
|
1030
|
+
function displayFieldLabel(field) {
|
|
1031
|
+
const words = field.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean);
|
|
1032
|
+
return words.map((word, index) => displayFieldLabelWord(word, index)).join(" ");
|
|
1033
|
+
}
|
|
1034
|
+
function displayFieldLabelWord(word, index) {
|
|
1035
|
+
const lower = word.toLowerCase();
|
|
1036
|
+
const acronyms = {
|
|
1037
|
+
dob: "DOB",
|
|
1038
|
+
id: "ID",
|
|
1039
|
+
ssn: "SSN",
|
|
1040
|
+
url: "URL",
|
|
1041
|
+
us: "US",
|
|
1042
|
+
zip: "ZIP"
|
|
1043
|
+
};
|
|
1044
|
+
if (lower in acronyms) {
|
|
1045
|
+
return acronyms[lower];
|
|
1046
|
+
}
|
|
1047
|
+
if (index > 0 && ["a", "an", "and", "for", "in", "of", "on", "or", "the", "to", "with"].includes(lower)) {
|
|
1048
|
+
return lower;
|
|
1049
|
+
}
|
|
1050
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
1051
|
+
}
|
|
1052
|
+
function cssString(value) {
|
|
1053
|
+
return globalThis.CSS?.escape?.(value) ?? value.replace(/["\\]/g, "\\$&");
|
|
1054
|
+
}
|
|
1055
|
+
function enhanceTabs(root, tabs, navigation, viewedSectionId, frameId, onPanelChange, onViewSection) {
|
|
1056
|
+
if (tabs.length === 0 || navigation === null || tabs.length !== navigation.orderedSectionIds.length) {
|
|
1057
|
+
return () => void 0;
|
|
1058
|
+
}
|
|
1059
|
+
const header = findTabHeader(root, tabs);
|
|
1060
|
+
if (header === void 0) {
|
|
1061
|
+
return () => void 0;
|
|
1062
|
+
}
|
|
1063
|
+
const buttons = [...header.children].filter(
|
|
1064
|
+
(child) => child instanceof HTMLButtonElement
|
|
1065
|
+
);
|
|
1066
|
+
const panel = header.nextElementSibling;
|
|
1067
|
+
if (buttons.length !== tabs.length || panel === null) {
|
|
1068
|
+
return () => void 0;
|
|
1069
|
+
}
|
|
1070
|
+
const statuses = tabStatuses(navigation);
|
|
1071
|
+
const sectionIds = navigation.orderedSectionIds;
|
|
1072
|
+
const enabled = statuses.map((status, index) => status === "future" ? -1 : index).filter((index) => index >= 0);
|
|
1073
|
+
const authoritative = statuses.findIndex((status) => status === "active");
|
|
1074
|
+
const requested = viewedSectionId === null ? -1 : sectionIds.indexOf(viewedSectionId);
|
|
1075
|
+
const selectedIndex = requested >= 0 && statuses[requested] !== "future" ? requested : authoritative >= 0 ? authoritative : enabled[0] ?? 0;
|
|
1076
|
+
const listeners = [];
|
|
1077
|
+
let synchronizingRenderer = false;
|
|
1078
|
+
header.dataset.a2uiTabList = "true";
|
|
1079
|
+
header.setAttribute("role", "tablist");
|
|
1080
|
+
header.setAttribute("aria-label", "Intake sections");
|
|
1081
|
+
panel.dataset.a2uiTabPanel = "true";
|
|
1082
|
+
panel.setAttribute("role", "tabpanel");
|
|
1083
|
+
panel.tabIndex = 0;
|
|
1084
|
+
panel.id = `${safeDomToken(frameId)}-tabpanel`;
|
|
1085
|
+
const activate = (index) => {
|
|
1086
|
+
const selected = buttons[index];
|
|
1087
|
+
if (selected === void 0 || statuses[index] === "future") {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
buttons.forEach((button, buttonIndex) => {
|
|
1091
|
+
const isSelected = buttonIndex === index;
|
|
1092
|
+
button.setAttribute("aria-selected", String(isSelected));
|
|
1093
|
+
button.tabIndex = isSelected ? 0 : -1;
|
|
1094
|
+
});
|
|
1095
|
+
panel.setAttribute("aria-labelledby", selected.id);
|
|
1096
|
+
onPanelChange();
|
|
1097
|
+
};
|
|
1098
|
+
buttons.forEach((button, index) => {
|
|
1099
|
+
button.dataset.a2uiTab = statuses[index];
|
|
1100
|
+
button.setAttribute("role", "tab");
|
|
1101
|
+
button.id = `${safeDomToken(frameId)}-tab-${index}`;
|
|
1102
|
+
button.setAttribute("aria-controls", panel.id);
|
|
1103
|
+
if (statuses[index] === "future") {
|
|
1104
|
+
button.disabled = true;
|
|
1105
|
+
button.setAttribute("aria-disabled", "true");
|
|
1106
|
+
button.tabIndex = -1;
|
|
1107
|
+
}
|
|
1108
|
+
const onClick = () => {
|
|
1109
|
+
activate(index);
|
|
1110
|
+
if (!synchronizingRenderer) {
|
|
1111
|
+
onViewSection(sectionIds[index]);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
const onKeyDown = (event) => {
|
|
1115
|
+
const destination = tabKeyboardDestination(
|
|
1116
|
+
event,
|
|
1117
|
+
index,
|
|
1118
|
+
enabled
|
|
1119
|
+
);
|
|
1120
|
+
if (destination === void 0) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
event.preventDefault();
|
|
1124
|
+
buttons[destination]?.focus();
|
|
1125
|
+
buttons[destination]?.click();
|
|
1126
|
+
};
|
|
1127
|
+
button.addEventListener("click", onClick);
|
|
1128
|
+
button.addEventListener("keydown", onKeyDown);
|
|
1129
|
+
listeners.push(() => {
|
|
1130
|
+
button.removeEventListener("click", onClick);
|
|
1131
|
+
button.removeEventListener("keydown", onKeyDown);
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
synchronizingRenderer = true;
|
|
1135
|
+
buttons[selectedIndex]?.click();
|
|
1136
|
+
synchronizingRenderer = false;
|
|
1137
|
+
activate(selectedIndex);
|
|
1138
|
+
return () => {
|
|
1139
|
+
for (const remove of listeners) {
|
|
1140
|
+
remove();
|
|
1141
|
+
}
|
|
1142
|
+
delete header.dataset.a2uiTabList;
|
|
1143
|
+
header.removeAttribute("role");
|
|
1144
|
+
header.removeAttribute("aria-label");
|
|
1145
|
+
delete panel.dataset.a2uiTabPanel;
|
|
1146
|
+
panel.removeAttribute("role");
|
|
1147
|
+
panel.removeAttribute("aria-labelledby");
|
|
1148
|
+
panel.removeAttribute("id");
|
|
1149
|
+
panel.removeAttribute("tabindex");
|
|
1150
|
+
buttons.forEach((button) => {
|
|
1151
|
+
delete button.dataset.a2uiTab;
|
|
1152
|
+
button.removeAttribute("role");
|
|
1153
|
+
button.removeAttribute("aria-selected");
|
|
1154
|
+
button.removeAttribute("aria-controls");
|
|
1155
|
+
button.removeAttribute("aria-disabled");
|
|
1156
|
+
button.removeAttribute("id");
|
|
1157
|
+
button.removeAttribute("tabindex");
|
|
1158
|
+
button.disabled = false;
|
|
1159
|
+
});
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function findTabHeader(root, tabs) {
|
|
1163
|
+
return [...root.querySelectorAll("div")].find((candidate) => {
|
|
1164
|
+
const directButtons = [...candidate.children].filter(
|
|
1165
|
+
(child) => child instanceof HTMLButtonElement
|
|
1166
|
+
);
|
|
1167
|
+
return directButtons.length === tabs.length && directButtons.every(
|
|
1168
|
+
(button, index) => button.textContent?.trim() === tabs[index]?.title
|
|
1169
|
+
);
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
function tabStatuses(navigation) {
|
|
1173
|
+
const completed = new Set(navigation.completedSectionIds);
|
|
1174
|
+
const future = new Set(navigation.futureSectionIds);
|
|
1175
|
+
return navigation.orderedSectionIds.map((sectionId) => {
|
|
1176
|
+
if (sectionId === navigation.activeSectionId) {
|
|
1177
|
+
return "active";
|
|
1178
|
+
}
|
|
1179
|
+
return completed.has(sectionId) ? "completed" : future.has(sectionId) ? "future" : "future";
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
function tabKeyboardDestination(event, current, enabled) {
|
|
1183
|
+
const currentEnabledIndex = enabled.indexOf(current);
|
|
1184
|
+
if (event.key === "Home") {
|
|
1185
|
+
return enabled[0];
|
|
1186
|
+
}
|
|
1187
|
+
if (event.key === "End") {
|
|
1188
|
+
return enabled.at(-1);
|
|
1189
|
+
}
|
|
1190
|
+
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") {
|
|
1191
|
+
return void 0;
|
|
1192
|
+
}
|
|
1193
|
+
const delta = event.key === "ArrowRight" ? 1 : -1;
|
|
1194
|
+
const start = currentEnabledIndex >= 0 ? currentEnabledIndex : 0;
|
|
1195
|
+
return enabled[(start + delta + enabled.length) % enabled.length];
|
|
1196
|
+
}
|
|
1197
|
+
function setPendingGroupsDisabled(root, pendingGroupIds) {
|
|
1198
|
+
if (pendingGroupIds.size === 0) {
|
|
1199
|
+
return () => void 0;
|
|
1200
|
+
}
|
|
1201
|
+
const originals = /* @__PURE__ */ new Map();
|
|
1202
|
+
for (const control of root.querySelectorAll(
|
|
1203
|
+
"[data-a2ui-group-id]"
|
|
1204
|
+
)) {
|
|
1205
|
+
if (!isDisableableControl(control) || !pendingGroupIds.has(control.dataset.a2uiGroupId ?? "")) {
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
originals.set(control, {
|
|
1209
|
+
disabled: control.disabled,
|
|
1210
|
+
busy: control.getAttribute("aria-busy"),
|
|
1211
|
+
pending: control.dataset.a2uiGroupPending
|
|
1212
|
+
});
|
|
1213
|
+
control.disabled = true;
|
|
1214
|
+
control.setAttribute("aria-busy", "true");
|
|
1215
|
+
control.dataset.a2uiGroupPending = "true";
|
|
1216
|
+
}
|
|
1217
|
+
return () => {
|
|
1218
|
+
for (const [control, original] of originals) {
|
|
1219
|
+
control.disabled = original.disabled;
|
|
1220
|
+
restoreAttribute(control, "aria-busy", original.busy);
|
|
1221
|
+
restoreDataset(
|
|
1222
|
+
control,
|
|
1223
|
+
"a2uiGroupPending",
|
|
1224
|
+
original.pending
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
function isDisableableControl(control) {
|
|
1230
|
+
return control instanceof HTMLButtonElement || control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement || control instanceof HTMLSelectElement;
|
|
1231
|
+
}
|
|
1232
|
+
function setInteractionDisabled(root, disabled) {
|
|
1233
|
+
if (!disabled) {
|
|
1234
|
+
root.removeAttribute("inert");
|
|
1235
|
+
return () => void 0;
|
|
1236
|
+
}
|
|
1237
|
+
root.setAttribute("inert", "");
|
|
1238
|
+
const controls = [
|
|
1239
|
+
...root.querySelectorAll("button, input, textarea, select")
|
|
1240
|
+
];
|
|
1241
|
+
for (const control of controls) {
|
|
1242
|
+
if (!control.disabled) {
|
|
1243
|
+
control.disabled = true;
|
|
1244
|
+
control.dataset.a2uiSessionDisabled = "true";
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return () => {
|
|
1248
|
+
root.removeAttribute("inert");
|
|
1249
|
+
for (const control of controls) {
|
|
1250
|
+
if (control.dataset.a2uiSessionDisabled === "true") {
|
|
1251
|
+
control.disabled = false;
|
|
1252
|
+
delete control.dataset.a2uiSessionDisabled;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function safeDomToken(value) {
|
|
1258
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
1259
|
+
}
|
|
1260
|
+
function restoreAttribute(element, name, value) {
|
|
1261
|
+
if (value === null) {
|
|
1262
|
+
element.removeAttribute(name);
|
|
1263
|
+
} else {
|
|
1264
|
+
element.setAttribute(name, value);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/session.ts
|
|
1269
|
+
import { A2uiMessageListSchema as A2uiMessageListSchema2 } from "@a2ui/web_core/v0_9";
|
|
1270
|
+
|
|
1271
|
+
// src/actions.ts
|
|
1272
|
+
var DEFAULT_ACK_TIMEOUT_MS = 1e4;
|
|
1273
|
+
var SAFE_DETAIL = /^[A-Za-z0-9_.:-]{1,96}$/;
|
|
1274
|
+
var MAX_IDENTIFIER_LENGTH = 256;
|
|
1275
|
+
var EMPTY_ACTION_STATE = {
|
|
1276
|
+
pendingRequests: [],
|
|
1277
|
+
lastResult: null
|
|
1278
|
+
};
|
|
1279
|
+
var A2UIClientActionBridge = class {
|
|
1280
|
+
#options;
|
|
1281
|
+
#pendingByKey = /* @__PURE__ */ new Map();
|
|
1282
|
+
#pendingByRequestId = /* @__PURE__ */ new Map();
|
|
1283
|
+
#lastResult = null;
|
|
1284
|
+
#publishQueue = Promise.resolve();
|
|
1285
|
+
#closed = false;
|
|
1286
|
+
constructor(options) {
|
|
1287
|
+
this.#options = options;
|
|
1288
|
+
}
|
|
1289
|
+
getSnapshot() {
|
|
1290
|
+
if (this.#pendingByKey.size === 0 && this.#lastResult === null) {
|
|
1291
|
+
return EMPTY_ACTION_STATE;
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
pendingRequests: [...this.#pendingByKey.values()].map((pending) => ({
|
|
1295
|
+
requestId: pending.requestId,
|
|
1296
|
+
action: pending.action,
|
|
1297
|
+
surfaceId: pending.surfaceId,
|
|
1298
|
+
stateId: pending.stateId,
|
|
1299
|
+
revision: pending.revision,
|
|
1300
|
+
deliveryStatus: pending.deliveryStatus,
|
|
1301
|
+
attemptCount: pending.attemptCount,
|
|
1302
|
+
...pending.groupId === void 0 ? {} : { groupId: pending.groupId }
|
|
1303
|
+
})),
|
|
1304
|
+
lastResult: this.#lastResult === null ? null : {
|
|
1305
|
+
...this.#lastResult,
|
|
1306
|
+
fieldErrors: this.#lastResult.fieldErrors.map((entry) => ({
|
|
1307
|
+
...entry
|
|
1308
|
+
})),
|
|
1309
|
+
...this.#lastResult.groupId === void 0 ? {} : { groupId: this.#lastResult.groupId }
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
handleRendererAction(event) {
|
|
1314
|
+
if (this.#closed || !this.#options.canPublish()) {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const metadata = this.#options.getSurfaceMetadata();
|
|
1318
|
+
if (metadata === null || metadata.activeStateId === null || event.surfaceId !== metadata.surfaceId) {
|
|
1319
|
+
this.#rejectRendererAction(event.surfaceId, "action_metadata_stale");
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const decoded = decodeRendererAction(event);
|
|
1323
|
+
if (decoded === null) {
|
|
1324
|
+
this.#rejectRendererAction(event.surfaceId, "action_context_invalid");
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const key = pendingKey(metadata.surfaceId, metadata.activeStateId);
|
|
1328
|
+
const existing = this.#pendingByKey.get(key);
|
|
1329
|
+
if (existing !== void 0) {
|
|
1330
|
+
if (existing.deliveryStatus === "retryable" && isSameLogicalAction(existing.envelope, decoded)) {
|
|
1331
|
+
this.#queueAttempt(existing);
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
this.#startLogicalAction(decoded, key);
|
|
1336
|
+
}
|
|
1337
|
+
/** Publish one strict server-issued owning-group commit. */
|
|
1338
|
+
handleFieldGroupCommit(group, fields) {
|
|
1339
|
+
if (this.#closed || !this.#options.canPublish()) {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
const metadata = this.#options.getSurfaceMetadata();
|
|
1343
|
+
if (metadata === null || metadata.activeStateId === null) {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
const expected = group.fields.map((binding) => binding.field);
|
|
1347
|
+
if (expected.length === 0 || Object.keys(fields).length !== expected.length || expected.some((field) => !Object.hasOwn(fields, field)) || group.mode === "current" && group.ownerStateId !== metadata.activeStateId || group.mode === "revision" && group.ownerStateId === metadata.activeStateId) {
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
const orderedFields = {};
|
|
1351
|
+
for (const field of expected) {
|
|
1352
|
+
const value = normalizeFieldValue(fields[field]);
|
|
1353
|
+
if (value === void 0) {
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
orderedFields[field] = value;
|
|
1357
|
+
}
|
|
1358
|
+
const key = groupPendingKey(metadata.surfaceId, group.groupId);
|
|
1359
|
+
if (this.#pendingByKey.has(key)) {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
const decoded = group.mode === "current" ? { action: "flow.submit", payload: { fields: orderedFields } } : {
|
|
1363
|
+
action: "flow.revise",
|
|
1364
|
+
payload: {
|
|
1365
|
+
targetStateId: group.ownerStateId,
|
|
1366
|
+
fields: orderedFields
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
return this.#startLogicalAction(decoded, key, group.groupId);
|
|
1370
|
+
}
|
|
1371
|
+
#startLogicalAction(decoded, key, groupId) {
|
|
1372
|
+
const metadata = this.#options.getSurfaceMetadata();
|
|
1373
|
+
if (metadata === null || metadata.activeStateId === null) {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
let requestId;
|
|
1377
|
+
try {
|
|
1378
|
+
requestId = createRequestId("req");
|
|
1379
|
+
} catch {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
const envelope = {
|
|
1383
|
+
envelopeVersion: ENVELOPE_VERSION,
|
|
1384
|
+
a2uiVersion: A2UI_PROTOCOL_VERSION,
|
|
1385
|
+
kind: "action",
|
|
1386
|
+
requestId,
|
|
1387
|
+
role: this.#options.participantRole ?? "member",
|
|
1388
|
+
surfaceId: metadata.surfaceId,
|
|
1389
|
+
stateId: metadata.activeStateId,
|
|
1390
|
+
revision: metadata.revision,
|
|
1391
|
+
action: decoded.action,
|
|
1392
|
+
...decoded.payload === void 0 ? {} : { payload: decoded.payload }
|
|
1393
|
+
};
|
|
1394
|
+
const encoded = encodeEnvelope(envelope);
|
|
1395
|
+
if (encoded === null) {
|
|
1396
|
+
this.#options.reportClientError(
|
|
1397
|
+
"message_malformed",
|
|
1398
|
+
envelope.surfaceId,
|
|
1399
|
+
"action_envelope_too_large"
|
|
1400
|
+
);
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
const pending = {
|
|
1404
|
+
requestId,
|
|
1405
|
+
action: envelope.action,
|
|
1406
|
+
surfaceId: envelope.surfaceId,
|
|
1407
|
+
stateId: envelope.stateId,
|
|
1408
|
+
revision: envelope.revision,
|
|
1409
|
+
deliveryStatus: "publishing",
|
|
1410
|
+
attemptCount: 0,
|
|
1411
|
+
generation: 0,
|
|
1412
|
+
envelope,
|
|
1413
|
+
encoded,
|
|
1414
|
+
key,
|
|
1415
|
+
...groupId === void 0 ? {} : { groupId }
|
|
1416
|
+
};
|
|
1417
|
+
this.#pendingByKey.set(key, pending);
|
|
1418
|
+
this.#pendingByRequestId.set(requestId, pending);
|
|
1419
|
+
this.#queueAttempt(pending);
|
|
1420
|
+
return true;
|
|
1421
|
+
}
|
|
1422
|
+
handleClientError(event) {
|
|
1423
|
+
if (this.#closed || !this.#options.canPublish()) {
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
let requestId;
|
|
1427
|
+
try {
|
|
1428
|
+
requestId = createRequestId("err");
|
|
1429
|
+
} catch {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const surfaceId = safeIdentifier(event.surfaceId);
|
|
1433
|
+
const detail = safeDetail(event.detail);
|
|
1434
|
+
const envelope = {
|
|
1435
|
+
envelopeVersion: ENVELOPE_VERSION,
|
|
1436
|
+
a2uiVersion: A2UI_PROTOCOL_VERSION,
|
|
1437
|
+
kind: "clientError",
|
|
1438
|
+
requestId,
|
|
1439
|
+
role: this.#options.participantRole ?? "member",
|
|
1440
|
+
errorCode: event.errorCode,
|
|
1441
|
+
...surfaceId === void 0 ? {} : { surfaceId },
|
|
1442
|
+
...detail === void 0 ? {} : { detail }
|
|
1443
|
+
};
|
|
1444
|
+
const encoded = encodeEnvelope(envelope);
|
|
1445
|
+
if (encoded !== null) {
|
|
1446
|
+
this.#enqueue(async () => {
|
|
1447
|
+
await this.#options.publish(encoded);
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
handleAck(envelope) {
|
|
1452
|
+
if (this.#closed) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
const pending = this.#pendingByRequestId.get(envelope.inResponseTo);
|
|
1456
|
+
if (pending === void 0) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
this.#removePending(pending);
|
|
1460
|
+
this.#lastResult = {
|
|
1461
|
+
requestId: pending.requestId,
|
|
1462
|
+
action: pending.action,
|
|
1463
|
+
surfaceId: pending.surfaceId,
|
|
1464
|
+
stateId: pending.stateId,
|
|
1465
|
+
revision: pending.revision,
|
|
1466
|
+
status: envelope.status,
|
|
1467
|
+
...envelope.reasonCode === void 0 ? {} : { reasonCode: envelope.reasonCode },
|
|
1468
|
+
fieldErrors: (envelope.fieldErrors ?? []).map((entry) => ({ ...entry })),
|
|
1469
|
+
...pending.groupId === void 0 ? {} : { groupId: pending.groupId }
|
|
1470
|
+
};
|
|
1471
|
+
this.#notify();
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
markReconnect() {
|
|
1475
|
+
if (this.#closed) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
let changed = false;
|
|
1479
|
+
for (const pending of this.#pendingByKey.values()) {
|
|
1480
|
+
this.#clearTimer(pending);
|
|
1481
|
+
pending.generation += 1;
|
|
1482
|
+
if (pending.deliveryStatus !== "retryable") {
|
|
1483
|
+
pending.deliveryStatus = "retryable";
|
|
1484
|
+
changed = true;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (changed) {
|
|
1488
|
+
this.#notify();
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
retryAfterRestore() {
|
|
1492
|
+
if (this.#closed || !this.#options.canPublish()) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
for (const pending of this.#pendingByKey.values()) {
|
|
1496
|
+
if (pending.deliveryStatus === "retryable") {
|
|
1497
|
+
this.#queueAttempt(pending);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
retryPendingAction(requestId) {
|
|
1502
|
+
if (this.#closed || !this.#options.canPublish()) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
const candidates = requestId === void 0 ? [...this.#pendingByKey.values()] : [this.#pendingByRequestId.get(requestId)].filter(
|
|
1506
|
+
(entry) => entry !== void 0
|
|
1507
|
+
);
|
|
1508
|
+
let retried = false;
|
|
1509
|
+
for (const pending of candidates) {
|
|
1510
|
+
if (pending.deliveryStatus === "retryable") {
|
|
1511
|
+
this.#queueAttempt(pending);
|
|
1512
|
+
retried = true;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return retried;
|
|
1516
|
+
}
|
|
1517
|
+
discardPending() {
|
|
1518
|
+
if (this.#pendingByKey.size === 0) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
for (const pending of this.#pendingByKey.values()) {
|
|
1522
|
+
this.#clearTimer(pending);
|
|
1523
|
+
pending.generation += 1;
|
|
1524
|
+
}
|
|
1525
|
+
this.#pendingByKey.clear();
|
|
1526
|
+
this.#pendingByRequestId.clear();
|
|
1527
|
+
this.#notify();
|
|
1528
|
+
}
|
|
1529
|
+
stop() {
|
|
1530
|
+
if (this.#closed) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
this.#closed = true;
|
|
1534
|
+
this.discardPending();
|
|
1535
|
+
this.#lastResult = null;
|
|
1536
|
+
}
|
|
1537
|
+
#rejectRendererAction(surfaceId, detail) {
|
|
1538
|
+
this.#options.reportClientError(
|
|
1539
|
+
"message_malformed",
|
|
1540
|
+
safeIdentifier(surfaceId),
|
|
1541
|
+
detail
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
#queueAttempt(pending) {
|
|
1545
|
+
if (this.#closed || !this.#options.canPublish()) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
this.#clearTimer(pending);
|
|
1549
|
+
pending.deliveryStatus = "publishing";
|
|
1550
|
+
pending.attemptCount += 1;
|
|
1551
|
+
pending.generation += 1;
|
|
1552
|
+
const generation = pending.generation;
|
|
1553
|
+
this.#notify();
|
|
1554
|
+
this.#enqueue(async () => {
|
|
1555
|
+
if (!this.#isCurrent(pending, generation)) {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
try {
|
|
1559
|
+
await this.#options.publish(pending.encoded);
|
|
1560
|
+
} catch {
|
|
1561
|
+
if (this.#isCurrent(pending, generation)) {
|
|
1562
|
+
pending.deliveryStatus = "retryable";
|
|
1563
|
+
this.#notify();
|
|
1564
|
+
}
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (!this.#isCurrent(pending, generation)) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
pending.deliveryStatus = "awaiting_ack";
|
|
1571
|
+
pending.timeoutId = globalThis.setTimeout(() => {
|
|
1572
|
+
if (this.#isCurrent(pending, generation)) {
|
|
1573
|
+
pending.timeoutId = void 0;
|
|
1574
|
+
pending.deliveryStatus = "retryable";
|
|
1575
|
+
this.#notify();
|
|
1576
|
+
}
|
|
1577
|
+
}, this.#ackTimeoutMs());
|
|
1578
|
+
this.#notify();
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
#enqueue(operation) {
|
|
1582
|
+
this.#publishQueue = this.#publishQueue.then(operation).catch(() => void 0);
|
|
1583
|
+
}
|
|
1584
|
+
#ackTimeoutMs() {
|
|
1585
|
+
const configured = this.#options.ackTimeoutMs;
|
|
1586
|
+
return typeof configured === "number" && Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_ACK_TIMEOUT_MS;
|
|
1587
|
+
}
|
|
1588
|
+
#isCurrent(pending, generation) {
|
|
1589
|
+
return !this.#closed && pending.generation === generation && this.#pendingByRequestId.get(pending.requestId) === pending;
|
|
1590
|
+
}
|
|
1591
|
+
#removePending(pending) {
|
|
1592
|
+
this.#clearTimer(pending);
|
|
1593
|
+
pending.generation += 1;
|
|
1594
|
+
this.#pendingByKey.delete(pending.key);
|
|
1595
|
+
this.#pendingByRequestId.delete(pending.requestId);
|
|
1596
|
+
}
|
|
1597
|
+
#clearTimer(pending) {
|
|
1598
|
+
if (pending.timeoutId !== void 0) {
|
|
1599
|
+
globalThis.clearTimeout(pending.timeoutId);
|
|
1600
|
+
pending.timeoutId = void 0;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
#notify() {
|
|
1604
|
+
this.#options.onStateChange(this.getSnapshot());
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
function decodeRendererAction(event) {
|
|
1608
|
+
if (!CLIENT_ACTIONS.includes(event.actionName)) {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
const action = event.actionName;
|
|
1612
|
+
const entries = Object.entries(event.context).sort(
|
|
1613
|
+
([left], [right]) => left.localeCompare(right)
|
|
1614
|
+
);
|
|
1615
|
+
if (entries.some(([key]) => key.length === 0)) {
|
|
1616
|
+
return null;
|
|
1617
|
+
}
|
|
1618
|
+
if (action === "flow.skip") {
|
|
1619
|
+
return entries.length === 0 ? { action } : null;
|
|
1620
|
+
}
|
|
1621
|
+
if (action === "flow.submit") {
|
|
1622
|
+
const fields2 = scalarFields(entries);
|
|
1623
|
+
return fields2 === null || Object.keys(fields2).length === 0 ? null : { action, payload: { fields: fields2 } };
|
|
1624
|
+
}
|
|
1625
|
+
if (action === "flow.list.add" || action === "flow.list.update" || action === "flow.list.delete") {
|
|
1626
|
+
const listPath = event.context.listPath;
|
|
1627
|
+
if (typeof listPath !== "string" || listPath.length === 0) {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
const itemId = event.context.itemId;
|
|
1631
|
+
if ((action === "flow.list.update" || action === "flow.list.delete") && (typeof itemId !== "string" || itemId.length === 0)) {
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
const fieldEntries2 = entries.filter(
|
|
1635
|
+
([key]) => key !== "listPath" && key !== "itemId"
|
|
1636
|
+
);
|
|
1637
|
+
const fields2 = scalarFields(fieldEntries2);
|
|
1638
|
+
if (action !== "flow.list.delete" && (fields2 === null || Object.keys(fields2).length === 0)) {
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
action,
|
|
1643
|
+
payload: {
|
|
1644
|
+
listPath,
|
|
1645
|
+
...typeof itemId === "string" ? { itemId } : {},
|
|
1646
|
+
...fields2 !== null && Object.keys(fields2).length > 0 ? { fields: fields2 } : {}
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
const targetStateId = event.context.targetStateId;
|
|
1651
|
+
if (typeof targetStateId !== "string" || targetStateId.length === 0) {
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
const fieldEntries = entries.filter(([key]) => key !== "targetStateId");
|
|
1655
|
+
const fields = scalarFields(fieldEntries);
|
|
1656
|
+
if (fields === null) {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
action,
|
|
1661
|
+
payload: {
|
|
1662
|
+
targetStateId,
|
|
1663
|
+
...Object.keys(fields).length === 0 ? {} : { fields }
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function scalarFields(entries) {
|
|
1668
|
+
const fields = {};
|
|
1669
|
+
for (const [key, value] of entries) {
|
|
1670
|
+
const normalized = normalizeFieldValue(value);
|
|
1671
|
+
if (normalized === void 0) {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
fields[key] = normalized;
|
|
1675
|
+
}
|
|
1676
|
+
return fields;
|
|
1677
|
+
}
|
|
1678
|
+
function isSameLogicalAction(envelope, candidate) {
|
|
1679
|
+
return envelope.action === candidate.action && JSON.stringify(envelope.payload) === JSON.stringify(candidate.payload);
|
|
1680
|
+
}
|
|
1681
|
+
function pendingKey(surfaceId, stateId) {
|
|
1682
|
+
return JSON.stringify([surfaceId, stateId]);
|
|
1683
|
+
}
|
|
1684
|
+
function groupPendingKey(surfaceId, groupId) {
|
|
1685
|
+
return JSON.stringify(["group", surfaceId, groupId]);
|
|
1686
|
+
}
|
|
1687
|
+
function isFieldValue2(value) {
|
|
1688
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
1689
|
+
}
|
|
1690
|
+
function normalizeFieldValue(value) {
|
|
1691
|
+
if (isFieldValue2(value)) {
|
|
1692
|
+
return value;
|
|
1693
|
+
}
|
|
1694
|
+
if (!Array.isArray(value)) {
|
|
1695
|
+
return void 0;
|
|
1696
|
+
}
|
|
1697
|
+
if (value.length === 0) {
|
|
1698
|
+
return null;
|
|
1699
|
+
}
|
|
1700
|
+
if (value.length !== 1) {
|
|
1701
|
+
return void 0;
|
|
1702
|
+
}
|
|
1703
|
+
return normalizeFieldValue(value[0]);
|
|
1704
|
+
}
|
|
1705
|
+
function encodeEnvelope(envelope) {
|
|
1706
|
+
let encoded;
|
|
1707
|
+
try {
|
|
1708
|
+
encoded = JSON.stringify(envelope);
|
|
1709
|
+
} catch {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
return new TextEncoder().encode(encoded).byteLength <= MAX_ENVELOPE_BYTES ? encoded : null;
|
|
1713
|
+
}
|
|
1714
|
+
function createRequestId(prefix) {
|
|
1715
|
+
return `${prefix}-${globalThis.crypto.randomUUID()}`;
|
|
1716
|
+
}
|
|
1717
|
+
function safeIdentifier(value) {
|
|
1718
|
+
return value !== void 0 && value.length > 0 && value.length <= MAX_IDENTIFIER_LENGTH ? value : void 0;
|
|
1719
|
+
}
|
|
1720
|
+
function safeDetail(value) {
|
|
1721
|
+
return value !== void 0 && SAFE_DETAIL.test(value) ? value : void 0;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/session.ts
|
|
1725
|
+
var DEFAULT_CAPABILITY_RETRY_SCHEDULE_MS = [250, 1e3, 2500, 5e3];
|
|
1726
|
+
var A2UISessionController = class {
|
|
1727
|
+
#room;
|
|
1728
|
+
#adapter;
|
|
1729
|
+
#isTrustedServerParticipant;
|
|
1730
|
+
#participantRole;
|
|
1731
|
+
#capabilityRetryScheduleMs;
|
|
1732
|
+
#actionBridge;
|
|
1733
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
1734
|
+
#sessionSnapshot = {
|
|
1735
|
+
status: "idle",
|
|
1736
|
+
restoring: false,
|
|
1737
|
+
surface: null,
|
|
1738
|
+
actions: { pendingRequests: [], lastResult: null },
|
|
1739
|
+
editing: emptyEditingSnapshot()
|
|
1740
|
+
};
|
|
1741
|
+
#startPromise;
|
|
1742
|
+
#deliveryQueue = Promise.resolve();
|
|
1743
|
+
#handlerRegistered = false;
|
|
1744
|
+
#listenersRegistered = false;
|
|
1745
|
+
#closed = false;
|
|
1746
|
+
#pendingSnapshotRequestId;
|
|
1747
|
+
#capabilityGeneration = 0;
|
|
1748
|
+
#restoreResetsSequence = false;
|
|
1749
|
+
#revisionBaseline;
|
|
1750
|
+
#unsubscribeActions;
|
|
1751
|
+
#unsubscribeClientErrors;
|
|
1752
|
+
#capabilityRetryTimers = [];
|
|
1753
|
+
#draftsByGroup = /* @__PURE__ */ new Map();
|
|
1754
|
+
#authoritativeByGroup = /* @__PURE__ */ new Map();
|
|
1755
|
+
#rejectedGroupIds = /* @__PURE__ */ new Set();
|
|
1756
|
+
#handledActionResultRequestId;
|
|
1757
|
+
constructor(options) {
|
|
1758
|
+
this.#room = options.room;
|
|
1759
|
+
this.#adapter = options.adapter;
|
|
1760
|
+
this.#isTrustedServerParticipant = options.isTrustedServerParticipant;
|
|
1761
|
+
this.#participantRole = options.participantRole ?? "member";
|
|
1762
|
+
this.#capabilityRetryScheduleMs = options.capabilityRetryScheduleMs ?? DEFAULT_CAPABILITY_RETRY_SCHEDULE_MS;
|
|
1763
|
+
this.#actionBridge = new A2UIClientActionBridge({
|
|
1764
|
+
publish: async (text) => this.#room.localParticipant.sendText(text, {
|
|
1765
|
+
topic: A2UI_CLIENT_TOPIC
|
|
1766
|
+
}),
|
|
1767
|
+
canPublish: () => !this.#closed && this.status === "connected" && !this.restoring,
|
|
1768
|
+
getSurfaceMetadata: () => this.surfaceMetadata,
|
|
1769
|
+
reportClientError: (errorCode, surfaceId, detail) => this.#adapter.reportClientError(errorCode, surfaceId, detail),
|
|
1770
|
+
onStateChange: (actions) => this.#handleActionStateChange(actions),
|
|
1771
|
+
participantRole: this.#participantRole,
|
|
1772
|
+
ackTimeoutMs: options.actionAckTimeoutMs
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
get status() {
|
|
1776
|
+
return this.#sessionSnapshot.status;
|
|
1777
|
+
}
|
|
1778
|
+
get restoring() {
|
|
1779
|
+
return this.#sessionSnapshot.restoring;
|
|
1780
|
+
}
|
|
1781
|
+
get surfaceMetadata() {
|
|
1782
|
+
return this.#sessionSnapshot.surface;
|
|
1783
|
+
}
|
|
1784
|
+
get actionState() {
|
|
1785
|
+
return this.#sessionSnapshot.actions;
|
|
1786
|
+
}
|
|
1787
|
+
/** Retry one or every delivery-failed/timed-out request with its original id. */
|
|
1788
|
+
retryPendingAction(requestId) {
|
|
1789
|
+
return this.#actionBridge.retryPendingAction(requestId);
|
|
1790
|
+
}
|
|
1791
|
+
/** Select the authoritative server-owned section without choosing workflow state. */
|
|
1792
|
+
returnToCurrentSection() {
|
|
1793
|
+
const authoritative = this.#sessionSnapshot.editing.authoritativeSectionId;
|
|
1794
|
+
if (authoritative === null) {
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
return this.viewSection(authoritative);
|
|
1798
|
+
}
|
|
1799
|
+
/** View one exact server-issued active/completed section locally. */
|
|
1800
|
+
viewSection(sectionId) {
|
|
1801
|
+
const navigation = this.surfaceMetadata?.sectionNavigation;
|
|
1802
|
+
if (navigation === null || navigation === void 0 || sectionId === this.#sessionSnapshot.editing.viewedSectionId || sectionId !== navigation.activeSectionId && !navigation.completedSectionIds.includes(sectionId)) {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
this.#replaceSnapshot({
|
|
1806
|
+
editing: {
|
|
1807
|
+
...this.#sessionSnapshot.editing,
|
|
1808
|
+
viewedSectionId: sectionId
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
return true;
|
|
1812
|
+
}
|
|
1813
|
+
/** Capture a renderer-owned group after a real user input/change event. */
|
|
1814
|
+
captureFieldGroupDraft(groupId) {
|
|
1815
|
+
const surface = this.surfaceMetadata;
|
|
1816
|
+
const group = surface?.fieldInteractions.find(
|
|
1817
|
+
(candidate) => candidate.groupId === groupId
|
|
1818
|
+
);
|
|
1819
|
+
if (surface === null || group === void 0 || this.#pendingGroupIds(this.actionState).has(groupId)) {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
const fields = group.fields.map((binding) => binding.field);
|
|
1823
|
+
const values = this.#adapter.getFieldValues(surface.surfaceId, fields);
|
|
1824
|
+
const authoritative = this.#authoritativeByGroup.get(groupId);
|
|
1825
|
+
if (values === null || authoritative === void 0) {
|
|
1826
|
+
return false;
|
|
1827
|
+
}
|
|
1828
|
+
if (fieldValuesEqual(values, authoritative, fields)) {
|
|
1829
|
+
this.#draftsByGroup.delete(groupId);
|
|
1830
|
+
this.#rejectedGroupIds.delete(groupId);
|
|
1831
|
+
} else {
|
|
1832
|
+
this.#draftsByGroup.set(groupId, {
|
|
1833
|
+
surfaceId: surface.surfaceId,
|
|
1834
|
+
fields,
|
|
1835
|
+
baseValues: { ...authoritative },
|
|
1836
|
+
values: { ...values }
|
|
1837
|
+
});
|
|
1838
|
+
this.#rejectedGroupIds.delete(groupId);
|
|
1839
|
+
}
|
|
1840
|
+
this.#replaceSnapshot({ editing: this.#editingSnapshot() });
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
/** Commit one complete owning group against the latest surface revision. */
|
|
1844
|
+
commitFieldGroup(groupId) {
|
|
1845
|
+
if (this.status !== "connected" || this.restoring) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
this.captureFieldGroupDraft(groupId);
|
|
1849
|
+
const surface = this.surfaceMetadata;
|
|
1850
|
+
const group = surface?.fieldInteractions.find(
|
|
1851
|
+
(candidate) => candidate.groupId === groupId
|
|
1852
|
+
);
|
|
1853
|
+
const draft = this.#draftsByGroup.get(groupId);
|
|
1854
|
+
const authoritative = this.#authoritativeByGroup.get(groupId);
|
|
1855
|
+
if (surface === null || group === void 0 || draft === void 0 || authoritative === void 0 || this.#pendingGroupIds(this.actionState).has(groupId) || fieldValuesEqual(draft.values, authoritative, draft.fields) || !this.#groupIsComplete(surface, group, draft.values)) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
return this.#actionBridge.handleFieldGroupCommit(group, draft.values);
|
|
1859
|
+
}
|
|
1860
|
+
getSnapshot = () => this.#sessionSnapshot;
|
|
1861
|
+
subscribe = (listener) => {
|
|
1862
|
+
this.#listeners.add(listener);
|
|
1863
|
+
return () => this.#listeners.delete(listener);
|
|
1864
|
+
};
|
|
1865
|
+
/** Register the server stream before announcing exact renderer capability. */
|
|
1866
|
+
async start() {
|
|
1867
|
+
if (this.#closed || this.status === "closed") {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (this.#startPromise === void 0) {
|
|
1871
|
+
this.#startPromise = this.#startInternal();
|
|
1872
|
+
}
|
|
1873
|
+
await this.#startPromise;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Detach only A2UI handlers/listeners. The shared room remains caller-owned.
|
|
1877
|
+
*/
|
|
1878
|
+
stop() {
|
|
1879
|
+
if (this.#closed) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
this.#closed = true;
|
|
1883
|
+
this.#capabilityGeneration += 1;
|
|
1884
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
1885
|
+
this.#clearCapabilityRetryTimers();
|
|
1886
|
+
this.#restoreResetsSequence = false;
|
|
1887
|
+
this.#actionBridge.stop();
|
|
1888
|
+
this.#detach();
|
|
1889
|
+
this.#adapter.clearSurfaces();
|
|
1890
|
+
this.#revisionBaseline = void 0;
|
|
1891
|
+
this.#clearDrafts();
|
|
1892
|
+
this.#replaceSnapshot({
|
|
1893
|
+
status: "closed",
|
|
1894
|
+
restoring: false,
|
|
1895
|
+
surface: null,
|
|
1896
|
+
actions: { pendingRequests: [], lastResult: null },
|
|
1897
|
+
editing: emptyEditingSnapshot()
|
|
1898
|
+
});
|
|
1899
|
+
this.#listeners.clear();
|
|
1900
|
+
}
|
|
1901
|
+
async #startInternal() {
|
|
1902
|
+
this.#replaceSnapshot({ status: "connecting" });
|
|
1903
|
+
try {
|
|
1904
|
+
this.#unsubscribeActions = this.#adapter.subscribeActions(
|
|
1905
|
+
this.#handleRendererAction
|
|
1906
|
+
);
|
|
1907
|
+
this.#unsubscribeClientErrors = this.#adapter.subscribeClientErrors(
|
|
1908
|
+
this.#handleClientError
|
|
1909
|
+
);
|
|
1910
|
+
this.#room.registerTextStreamHandler(
|
|
1911
|
+
A2UI_SERVER_TOPIC,
|
|
1912
|
+
this.#handleTextStream
|
|
1913
|
+
);
|
|
1914
|
+
this.#handlerRegistered = true;
|
|
1915
|
+
this.#listenersRegistered = true;
|
|
1916
|
+
this.#room.on("reconnected", this.#handleReconnected);
|
|
1917
|
+
this.#room.on("disconnected", this.#handleDisconnected);
|
|
1918
|
+
} catch {
|
|
1919
|
+
this.#closeAfterStartupFailure();
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
const requestId = await this.#announceCapabilities();
|
|
1923
|
+
if (requestId === void 0) {
|
|
1924
|
+
this.#closeAfterStartupFailure();
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (!this.#closed) {
|
|
1928
|
+
this.#replaceSnapshot({ status: "connected" });
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
#closeAfterStartupFailure() {
|
|
1932
|
+
this.#closed = true;
|
|
1933
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
1934
|
+
this.#clearCapabilityRetryTimers();
|
|
1935
|
+
this.#actionBridge.stop();
|
|
1936
|
+
this.#detach();
|
|
1937
|
+
this.#adapter.clearSurfaces();
|
|
1938
|
+
this.#revisionBaseline = void 0;
|
|
1939
|
+
this.#clearDrafts();
|
|
1940
|
+
this.#replaceSnapshot({
|
|
1941
|
+
status: "closed",
|
|
1942
|
+
restoring: false,
|
|
1943
|
+
surface: null,
|
|
1944
|
+
actions: { pendingRequests: [], lastResult: null },
|
|
1945
|
+
editing: emptyEditingSnapshot()
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
#detach() {
|
|
1949
|
+
this.#clearCapabilityRetryTimers();
|
|
1950
|
+
this.#unsubscribeActions?.();
|
|
1951
|
+
this.#unsubscribeActions = void 0;
|
|
1952
|
+
this.#unsubscribeClientErrors?.();
|
|
1953
|
+
this.#unsubscribeClientErrors = void 0;
|
|
1954
|
+
if (this.#handlerRegistered) {
|
|
1955
|
+
try {
|
|
1956
|
+
this.#room.unregisterTextStreamHandler(A2UI_SERVER_TOPIC);
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
this.#handlerRegistered = false;
|
|
1960
|
+
}
|
|
1961
|
+
if (this.#listenersRegistered) {
|
|
1962
|
+
try {
|
|
1963
|
+
this.#room.off("reconnected", this.#handleReconnected);
|
|
1964
|
+
} catch {
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
this.#room.off("disconnected", this.#handleDisconnected);
|
|
1968
|
+
} catch {
|
|
1969
|
+
}
|
|
1970
|
+
this.#listenersRegistered = false;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
#handleTextStream = (reader, participant) => {
|
|
1974
|
+
if (this.#closed || !this.#handlerRegistered) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
let trusted = false;
|
|
1978
|
+
try {
|
|
1979
|
+
trusted = isNonEmptyString2(participant?.identity) && this.#isTrustedServerParticipant(participant);
|
|
1980
|
+
} catch {
|
|
1981
|
+
trusted = false;
|
|
1982
|
+
}
|
|
1983
|
+
if (!trusted) {
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
this.#deliveryQueue = this.#deliveryQueue.then(async () => this.#readAndProcess(reader)).catch(() => {
|
|
1987
|
+
this.#reportInboundError("message_malformed", "delivery_failed");
|
|
1988
|
+
});
|
|
1989
|
+
};
|
|
1990
|
+
#handleRendererAction = (event) => {
|
|
1991
|
+
this.#actionBridge.handleRendererAction(event);
|
|
1992
|
+
};
|
|
1993
|
+
#handleClientError = (event) => {
|
|
1994
|
+
this.#actionBridge.handleClientError(event);
|
|
1995
|
+
};
|
|
1996
|
+
async #readAndProcess(reader) {
|
|
1997
|
+
if (this.#closed) {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (typeof reader.info?.size === "number" && reader.info.size > MAX_ENVELOPE_BYTES) {
|
|
2001
|
+
this.#reportInboundError("message_malformed", "envelope_too_large");
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
let text;
|
|
2005
|
+
try {
|
|
2006
|
+
text = await reader.readAll();
|
|
2007
|
+
} catch {
|
|
2008
|
+
this.#reportInboundError("message_malformed", "stream_read_failed");
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (this.#closed || typeof text !== "string") {
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
if (new TextEncoder().encode(text).byteLength > MAX_ENVELOPE_BYTES) {
|
|
2015
|
+
this.#reportInboundError("message_malformed", "envelope_too_large");
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const decoded = decodeServerEnvelope(text);
|
|
2019
|
+
if (!decoded.ok) {
|
|
2020
|
+
this.#reportInboundError(decoded.errorCode, decoded.detail);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
await this.#processEnvelope(decoded.envelope);
|
|
2024
|
+
}
|
|
2025
|
+
async #processEnvelope(envelope) {
|
|
2026
|
+
if (this.#closed) {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
const correlated = this.#pendingSnapshotRequestId !== void 0 && "inResponseTo" in envelope && envelope.inResponseTo === this.#pendingSnapshotRequestId;
|
|
2030
|
+
if (correlated && envelope.kind === "snapshot") {
|
|
2031
|
+
await this.#applyAuthoritativeSnapshot(envelope);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (correlated && envelope.kind === "ack") {
|
|
2035
|
+
if (await this.#handleCapabilityAck(envelope)) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
if (this.status === "unsupported") {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (this.restoring && envelope.kind !== "ack") {
|
|
2043
|
+
if (this.#pendingSnapshotRequestId === void 0) {
|
|
2044
|
+
await this.#announceCapabilities();
|
|
2045
|
+
}
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (envelope.seq <= this.#adapter.lastSeq) {
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (envelope.kind === "ack") {
|
|
2052
|
+
if (this.#adapter.applyServerEnvelope(envelope) && !this.restoring) {
|
|
2053
|
+
this.#actionBridge.handleAck(envelope);
|
|
2054
|
+
}
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
if (envelope.kind === "snapshot") {
|
|
2058
|
+
await this.#applyAuthoritativeSnapshot(envelope);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
await this.#applyContiguousUpdate(envelope);
|
|
2062
|
+
}
|
|
2063
|
+
async #applyContiguousUpdate(envelope) {
|
|
2064
|
+
const baseline = this.#revisionBaseline;
|
|
2065
|
+
if (baseline !== void 0) {
|
|
2066
|
+
if (baseline.surfaceId !== envelope.surfaceId) {
|
|
2067
|
+
await this.#enterRestoreBarrier();
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
if (envelope.revision <= baseline.revision) {
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (envelope.revision !== baseline.revision + 1) {
|
|
2074
|
+
await this.#enterRestoreBarrier();
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (!this.#adapter.applyServerEnvelope(envelope)) {
|
|
2079
|
+
await this.#enterRestoreBarrier();
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
this.#revisionBaseline = {
|
|
2083
|
+
surfaceId: envelope.surfaceId,
|
|
2084
|
+
revision: envelope.revision
|
|
2085
|
+
};
|
|
2086
|
+
const surface = this.#metadataAfterApply(envelope);
|
|
2087
|
+
this.#reconcileDrafts(envelope, surface);
|
|
2088
|
+
this.#replaceSnapshot({
|
|
2089
|
+
surface,
|
|
2090
|
+
editing: this.#editingSnapshot(surface)
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
async #applyAuthoritativeSnapshot(envelope) {
|
|
2094
|
+
const correlated = this.#pendingSnapshotRequestId !== void 0 && envelope.inResponseTo === this.#pendingSnapshotRequestId;
|
|
2095
|
+
if (this.restoring && !correlated) {
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (correlated && this.#restoreResetsSequence) {
|
|
2099
|
+
this.#adapter.resetSequence();
|
|
2100
|
+
this.#restoreResetsSequence = false;
|
|
2101
|
+
} else if (envelope.seq <= this.#adapter.lastSeq) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (!this.#adapter.applyServerEnvelope(envelope)) {
|
|
2105
|
+
if (correlated) {
|
|
2106
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
2107
|
+
}
|
|
2108
|
+
await this.#enterRestoreBarrier();
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
this.#revisionBaseline = {
|
|
2112
|
+
surfaceId: envelope.surfaceId,
|
|
2113
|
+
revision: envelope.revision
|
|
2114
|
+
};
|
|
2115
|
+
this.#clearCapabilityRetryTimers();
|
|
2116
|
+
if (correlated) {
|
|
2117
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
2118
|
+
}
|
|
2119
|
+
const surface = this.#metadataAfterApply(envelope);
|
|
2120
|
+
this.#reconcileDrafts(envelope, surface);
|
|
2121
|
+
this.#replaceSnapshot({
|
|
2122
|
+
status: "connected",
|
|
2123
|
+
restoring: false,
|
|
2124
|
+
surface,
|
|
2125
|
+
editing: this.#editingSnapshot(surface)
|
|
2126
|
+
});
|
|
2127
|
+
this.#actionBridge.retryAfterRestore();
|
|
2128
|
+
}
|
|
2129
|
+
async #handleCapabilityAck(envelope) {
|
|
2130
|
+
if (envelope.status === "rejected" && (envelope.reasonCode === "a2ui_version_unsupported" || envelope.reasonCode === "catalog_unsupported")) {
|
|
2131
|
+
if (this.#restoreResetsSequence) {
|
|
2132
|
+
this.#adapter.resetSequence();
|
|
2133
|
+
this.#restoreResetsSequence = false;
|
|
2134
|
+
}
|
|
2135
|
+
this.#adapter.applyServerEnvelope(envelope);
|
|
2136
|
+
this.#adapter.clearSurfaces();
|
|
2137
|
+
this.#actionBridge.discardPending();
|
|
2138
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
2139
|
+
this.#clearCapabilityRetryTimers();
|
|
2140
|
+
this.#revisionBaseline = void 0;
|
|
2141
|
+
this.#clearDrafts();
|
|
2142
|
+
this.#replaceSnapshot({
|
|
2143
|
+
status: "unsupported",
|
|
2144
|
+
restoring: false,
|
|
2145
|
+
surface: null
|
|
2146
|
+
});
|
|
2147
|
+
return true;
|
|
2148
|
+
}
|
|
2149
|
+
if (envelope.status === "accepted" && envelope.noSurface === true) {
|
|
2150
|
+
if (this.#restoreResetsSequence) {
|
|
2151
|
+
this.#adapter.resetSequence();
|
|
2152
|
+
this.#restoreResetsSequence = false;
|
|
2153
|
+
}
|
|
2154
|
+
this.#adapter.applyServerEnvelope(envelope);
|
|
2155
|
+
this.#adapter.clearSurfaces();
|
|
2156
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
2157
|
+
this.#clearCapabilityRetryTimers();
|
|
2158
|
+
this.#revisionBaseline = void 0;
|
|
2159
|
+
this.#clearDrafts();
|
|
2160
|
+
this.#replaceSnapshot({
|
|
2161
|
+
status: "connected",
|
|
2162
|
+
restoring: false,
|
|
2163
|
+
surface: null
|
|
2164
|
+
});
|
|
2165
|
+
this.#actionBridge.retryAfterRestore();
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
return false;
|
|
2169
|
+
}
|
|
2170
|
+
async #enterRestoreBarrier() {
|
|
2171
|
+
if (this.#closed) {
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
if (!this.restoring) {
|
|
2175
|
+
this.#actionBridge.markReconnect();
|
|
2176
|
+
this.#replaceSnapshot({ restoring: true });
|
|
2177
|
+
}
|
|
2178
|
+
if (this.#pendingSnapshotRequestId === void 0) {
|
|
2179
|
+
await this.#announceCapabilities();
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
#metadataAfterApply(envelope) {
|
|
2183
|
+
if (this.#adapter.getSurface(envelope.surfaceId) === void 0) {
|
|
2184
|
+
return null;
|
|
2185
|
+
}
|
|
2186
|
+
return {
|
|
2187
|
+
surfaceId: envelope.surfaceId,
|
|
2188
|
+
activeStateId: envelope.activeStateId ?? null,
|
|
2189
|
+
revision: envelope.revision,
|
|
2190
|
+
sectionNavigation: cloneSectionNavigation(
|
|
2191
|
+
envelope.sectionNavigation ?? null
|
|
2192
|
+
),
|
|
2193
|
+
fieldInteractions: cloneFieldInteractions(
|
|
2194
|
+
envelope.fieldInteractions ?? []
|
|
2195
|
+
)
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
#handleActionStateChange(actions) {
|
|
2199
|
+
const result = actions.lastResult;
|
|
2200
|
+
let acceptedGroup = false;
|
|
2201
|
+
let rejectedGroupId;
|
|
2202
|
+
if (result?.groupId !== void 0 && result.requestId !== this.#handledActionResultRequestId) {
|
|
2203
|
+
this.#handledActionResultRequestId = result.requestId;
|
|
2204
|
+
if (result.status === "accepted") {
|
|
2205
|
+
acceptedGroup = true;
|
|
2206
|
+
const acceptedDraft = this.#draftsByGroup.get(result.groupId);
|
|
2207
|
+
if (acceptedDraft !== void 0) {
|
|
2208
|
+
this.#authoritativeByGroup.set(result.groupId, {
|
|
2209
|
+
...acceptedDraft.values
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
this.#draftsByGroup.delete(result.groupId);
|
|
2213
|
+
this.#rejectedGroupIds.delete(result.groupId);
|
|
2214
|
+
} else if (this.#draftsByGroup.has(result.groupId)) {
|
|
2215
|
+
this.#rejectedGroupIds.add(result.groupId);
|
|
2216
|
+
rejectedGroupId = result.groupId;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
let editing = this.#editingSnapshot(void 0, actions);
|
|
2220
|
+
if (rejectedGroupId !== void 0) {
|
|
2221
|
+
const rejectedSection = this.surfaceMetadata?.fieldInteractions.find(
|
|
2222
|
+
(group) => group.groupId === rejectedGroupId
|
|
2223
|
+
)?.sectionId;
|
|
2224
|
+
if (rejectedSection !== void 0) {
|
|
2225
|
+
editing = { ...editing, viewedSectionId: rejectedSection };
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (acceptedGroup && editing.dirtyGroupIds.length === 0 && editing.pendingGroupIds.length === 0 && editing.rejectedGroupIds.length === 0) {
|
|
2229
|
+
editing = {
|
|
2230
|
+
...editing,
|
|
2231
|
+
viewedSectionId: editing.authoritativeSectionId
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
this.#replaceSnapshot({
|
|
2235
|
+
actions,
|
|
2236
|
+
editing
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
#pendingGroupIds(actions) {
|
|
2240
|
+
return new Set(
|
|
2241
|
+
actions.pendingRequests.flatMap(
|
|
2242
|
+
(pending) => pending.groupId === void 0 ? [] : [pending.groupId]
|
|
2243
|
+
)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
#editingSnapshot(surface = void 0, actions = this.#sessionSnapshot.actions) {
|
|
2247
|
+
const resolvedSurface = surface === void 0 ? this.#sessionSnapshot.surface : surface;
|
|
2248
|
+
if (resolvedSurface === null) {
|
|
2249
|
+
return emptyEditingSnapshot();
|
|
2250
|
+
}
|
|
2251
|
+
const pending = this.#pendingGroupIds(actions);
|
|
2252
|
+
const order = resolvedSurface.fieldInteractions.map(
|
|
2253
|
+
(group) => group.groupId
|
|
2254
|
+
);
|
|
2255
|
+
const ordered = (ids) => {
|
|
2256
|
+
const values = new Set(ids);
|
|
2257
|
+
return [
|
|
2258
|
+
...order.filter((groupId) => values.delete(groupId)),
|
|
2259
|
+
...[...values].sort()
|
|
2260
|
+
];
|
|
2261
|
+
};
|
|
2262
|
+
const rejected = new Set(
|
|
2263
|
+
[...this.#rejectedGroupIds].filter(
|
|
2264
|
+
(groupId) => this.#draftsByGroup.has(groupId)
|
|
2265
|
+
)
|
|
2266
|
+
);
|
|
2267
|
+
const dirty = [...this.#draftsByGroup.keys()].filter(
|
|
2268
|
+
(groupId) => !pending.has(groupId) && !rejected.has(groupId)
|
|
2269
|
+
);
|
|
2270
|
+
const authoritative = resolvedSurface.sectionNavigation?.activeSectionId ?? null;
|
|
2271
|
+
const authoritativeChanged = authoritative !== this.#sessionSnapshot.editing.authoritativeSectionId;
|
|
2272
|
+
const viewed = this.#sessionSnapshot.editing.viewedSectionId;
|
|
2273
|
+
const viewedStillAvailable = viewed !== null && (viewed === authoritative || (resolvedSurface.sectionNavigation?.completedSectionIds.includes(
|
|
2274
|
+
viewed
|
|
2275
|
+
) ?? false));
|
|
2276
|
+
const preserveView = dirty.length > 0 || pending.size > 0 || rejected.size > 0;
|
|
2277
|
+
return {
|
|
2278
|
+
authoritativeSectionId: authoritative,
|
|
2279
|
+
viewedSectionId: preserveView || !authoritativeChanged && viewedStillAvailable ? viewed ?? authoritative : authoritative,
|
|
2280
|
+
dirtyGroupIds: ordered(dirty),
|
|
2281
|
+
pendingGroupIds: ordered(pending),
|
|
2282
|
+
rejectedGroupIds: ordered(rejected)
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
#groupIsComplete(surface, group, values) {
|
|
2286
|
+
const descriptors = new Map(
|
|
2287
|
+
this.#adapter.getSurfaceStructure(surface.surfaceId).fields.map((field) => [field.componentId, field])
|
|
2288
|
+
);
|
|
2289
|
+
return group.fields.every((binding) => {
|
|
2290
|
+
const descriptor = descriptors.get(binding.componentId);
|
|
2291
|
+
const value = values[binding.field];
|
|
2292
|
+
return descriptor?.field === binding.field && isFieldValue3(value) && (!descriptor.required || !isIncompleteRequiredValue(value));
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
#reconcileDrafts(envelope, surface) {
|
|
2296
|
+
if (surface === null) {
|
|
2297
|
+
this.#clearDrafts();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
const incoming = envelopeRootFieldValues(envelope);
|
|
2301
|
+
const presentGroups = new Set(
|
|
2302
|
+
surface.fieldInteractions.map((group) => group.groupId)
|
|
2303
|
+
);
|
|
2304
|
+
const pending = this.#pendingGroupIds(this.actionState);
|
|
2305
|
+
for (const group of surface.fieldInteractions) {
|
|
2306
|
+
const fields = group.fields.map((binding) => binding.field);
|
|
2307
|
+
const incomingValues = selectFieldValues(incoming, fields);
|
|
2308
|
+
const authoritative = incomingValues ?? this.#authoritativeByGroup.get(group.groupId) ?? this.#adapter.getFieldValues(surface.surfaceId, fields);
|
|
2309
|
+
if (authoritative !== null && authoritative !== void 0) {
|
|
2310
|
+
this.#authoritativeByGroup.set(group.groupId, {
|
|
2311
|
+
...authoritative
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
const draft = this.#draftsByGroup.get(group.groupId);
|
|
2315
|
+
if (draft === void 0) {
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
if (draft.surfaceId !== surface.surfaceId || !arraysEqual(draft.fields, fields)) {
|
|
2319
|
+
this.#draftsByGroup.delete(group.groupId);
|
|
2320
|
+
this.#rejectedGroupIds.delete(group.groupId);
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
if (authoritative !== null && authoritative !== void 0 && fieldValuesEqual(draft.values, authoritative, fields) && !pending.has(group.groupId)) {
|
|
2324
|
+
this.#draftsByGroup.delete(group.groupId);
|
|
2325
|
+
this.#rejectedGroupIds.delete(group.groupId);
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (authoritative !== null && authoritative !== void 0 && !pending.has(group.groupId) && !fieldValuesEqual(draft.baseValues, authoritative, fields)) {
|
|
2329
|
+
this.#draftsByGroup.delete(group.groupId);
|
|
2330
|
+
this.#rejectedGroupIds.delete(group.groupId);
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
this.#adapter.applyFieldValues(surface.surfaceId, draft.values);
|
|
2334
|
+
}
|
|
2335
|
+
for (const groupId of [...this.#authoritativeByGroup.keys()]) {
|
|
2336
|
+
if (!presentGroups.has(groupId)) {
|
|
2337
|
+
this.#authoritativeByGroup.delete(groupId);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
for (const groupId of [...this.#draftsByGroup.keys()]) {
|
|
2341
|
+
if (!presentGroups.has(groupId) && !pending.has(groupId)) {
|
|
2342
|
+
this.#draftsByGroup.delete(groupId);
|
|
2343
|
+
this.#rejectedGroupIds.delete(groupId);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
#clearDrafts() {
|
|
2348
|
+
this.#draftsByGroup.clear();
|
|
2349
|
+
this.#authoritativeByGroup.clear();
|
|
2350
|
+
this.#rejectedGroupIds.clear();
|
|
2351
|
+
this.#handledActionResultRequestId = void 0;
|
|
2352
|
+
}
|
|
2353
|
+
#handleReconnected = () => {
|
|
2354
|
+
if (this.#closed) {
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
void this.#beginReconnectRestore();
|
|
2358
|
+
};
|
|
2359
|
+
async #beginReconnectRestore() {
|
|
2360
|
+
if (this.#closed) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (this.#restoreResetsSequence && this.restoring && this.#pendingSnapshotRequestId !== void 0) {
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
2367
|
+
this.#restoreResetsSequence = true;
|
|
2368
|
+
this.#actionBridge.markReconnect();
|
|
2369
|
+
this.#replaceSnapshot({ restoring: true });
|
|
2370
|
+
await this.#announceCapabilities();
|
|
2371
|
+
}
|
|
2372
|
+
#handleDisconnected = () => {
|
|
2373
|
+
this.stop();
|
|
2374
|
+
};
|
|
2375
|
+
async #announceCapabilities() {
|
|
2376
|
+
if (this.#closed) {
|
|
2377
|
+
return void 0;
|
|
2378
|
+
}
|
|
2379
|
+
let requestId;
|
|
2380
|
+
try {
|
|
2381
|
+
requestId = createCapabilityRequestId();
|
|
2382
|
+
} catch {
|
|
2383
|
+
return void 0;
|
|
2384
|
+
}
|
|
2385
|
+
const envelope = {
|
|
2386
|
+
envelopeVersion: ENVELOPE_VERSION,
|
|
2387
|
+
kind: "capabilities",
|
|
2388
|
+
requestId,
|
|
2389
|
+
role: this.#participantRole,
|
|
2390
|
+
supportedA2uiVersions: [A2UI_PROTOCOL_VERSION],
|
|
2391
|
+
supportedCatalogIds: this.#adapter.supportedCatalogIds,
|
|
2392
|
+
renderer: {
|
|
2393
|
+
name: this.#adapter.rendererName,
|
|
2394
|
+
version: this.#adapter.rendererVersion
|
|
2395
|
+
},
|
|
2396
|
+
requestSnapshot: true
|
|
2397
|
+
};
|
|
2398
|
+
this.#pendingSnapshotRequestId = requestId;
|
|
2399
|
+
const generation = ++this.#capabilityGeneration;
|
|
2400
|
+
try {
|
|
2401
|
+
const encoded = JSON.stringify(envelope);
|
|
2402
|
+
await this.#room.localParticipant.sendText(encoded, {
|
|
2403
|
+
topic: A2UI_CAPABILITIES_TOPIC
|
|
2404
|
+
});
|
|
2405
|
+
this.#scheduleCapabilityRetries(encoded, requestId, generation);
|
|
2406
|
+
} catch {
|
|
2407
|
+
if (this.#pendingSnapshotRequestId === requestId) {
|
|
2408
|
+
this.#pendingSnapshotRequestId = void 0;
|
|
2409
|
+
}
|
|
2410
|
+
return void 0;
|
|
2411
|
+
}
|
|
2412
|
+
return requestId;
|
|
2413
|
+
}
|
|
2414
|
+
#scheduleCapabilityRetries(encoded, requestId, generation) {
|
|
2415
|
+
this.#clearCapabilityRetryTimers();
|
|
2416
|
+
for (const retryAfterMs of this.#capabilityRetryScheduleMs) {
|
|
2417
|
+
const timer = globalThis.setTimeout(() => {
|
|
2418
|
+
if (this.#closed || generation !== this.#capabilityGeneration || this.#pendingSnapshotRequestId !== requestId || !this.#handlerRegistered) {
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
void this.#room.localParticipant.sendText(encoded, { topic: A2UI_CAPABILITIES_TOPIC }).catch(() => void 0);
|
|
2422
|
+
}, retryAfterMs);
|
|
2423
|
+
this.#capabilityRetryTimers.push(timer);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
#clearCapabilityRetryTimers() {
|
|
2427
|
+
for (const timer of this.#capabilityRetryTimers) {
|
|
2428
|
+
globalThis.clearTimeout(timer);
|
|
2429
|
+
}
|
|
2430
|
+
this.#capabilityRetryTimers = [];
|
|
2431
|
+
}
|
|
2432
|
+
#reportInboundError(errorCode, detail) {
|
|
2433
|
+
if (!this.#closed) {
|
|
2434
|
+
this.#adapter.reportClientError(errorCode, void 0, detail);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
#replaceSnapshot(patch) {
|
|
2438
|
+
const nextSurface = patch.surface === void 0 ? this.#sessionSnapshot.surface : patch.surface;
|
|
2439
|
+
const nextEditing = patch.editing ?? editingAfterSurface(this.#sessionSnapshot.editing, patch.surface);
|
|
2440
|
+
this.#sessionSnapshot = {
|
|
2441
|
+
status: patch.status ?? this.#sessionSnapshot.status,
|
|
2442
|
+
restoring: patch.restoring ?? this.#sessionSnapshot.restoring,
|
|
2443
|
+
surface: patch.surface === void 0 ? this.#sessionSnapshot.surface : patch.surface,
|
|
2444
|
+
actions: patch.actions ?? this.#sessionSnapshot.actions,
|
|
2445
|
+
editing: nextSurface === null ? emptyEditingSnapshot() : nextEditing
|
|
2446
|
+
};
|
|
2447
|
+
for (const listener of this.#listeners) {
|
|
2448
|
+
listener();
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
function createCapabilityRequestId() {
|
|
2453
|
+
return `cap-${globalThis.crypto.randomUUID()}`;
|
|
2454
|
+
}
|
|
2455
|
+
function cloneSectionNavigation(navigation) {
|
|
2456
|
+
if (navigation === null) {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
return {
|
|
2460
|
+
orderedSectionIds: [...navigation.orderedSectionIds],
|
|
2461
|
+
activeSectionId: navigation.activeSectionId,
|
|
2462
|
+
completedSectionIds: [...navigation.completedSectionIds],
|
|
2463
|
+
futureSectionIds: [...navigation.futureSectionIds]
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
function cloneFieldInteractions(groups) {
|
|
2467
|
+
return groups.map((group) => ({
|
|
2468
|
+
groupId: group.groupId,
|
|
2469
|
+
ownerStateId: group.ownerStateId,
|
|
2470
|
+
...group.sectionId === void 0 ? {} : { sectionId: group.sectionId },
|
|
2471
|
+
mode: group.mode,
|
|
2472
|
+
fields: group.fields.map((field) => ({ ...field }))
|
|
2473
|
+
}));
|
|
2474
|
+
}
|
|
2475
|
+
function emptyEditingSnapshot() {
|
|
2476
|
+
return {
|
|
2477
|
+
authoritativeSectionId: null,
|
|
2478
|
+
viewedSectionId: null,
|
|
2479
|
+
dirtyGroupIds: [],
|
|
2480
|
+
pendingGroupIds: [],
|
|
2481
|
+
rejectedGroupIds: []
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
function editingAfterSurface(editing, surface) {
|
|
2485
|
+
if (surface === void 0) {
|
|
2486
|
+
return editing;
|
|
2487
|
+
}
|
|
2488
|
+
if (surface === null) {
|
|
2489
|
+
return emptyEditingSnapshot();
|
|
2490
|
+
}
|
|
2491
|
+
const authoritative = surface.sectionNavigation?.activeSectionId ?? null;
|
|
2492
|
+
const viewedStillAvailable = editing.viewedSectionId !== null && (editing.viewedSectionId === authoritative || (surface.sectionNavigation?.completedSectionIds.includes(
|
|
2493
|
+
editing.viewedSectionId
|
|
2494
|
+
) ?? false));
|
|
2495
|
+
const preserveView = editing.dirtyGroupIds.length > 0 || editing.pendingGroupIds.length > 0 || editing.rejectedGroupIds.length > 0 || editing.authoritativeSectionId === authoritative && viewedStillAvailable;
|
|
2496
|
+
return {
|
|
2497
|
+
...editing,
|
|
2498
|
+
authoritativeSectionId: authoritative,
|
|
2499
|
+
viewedSectionId: preserveView ? editing.viewedSectionId : authoritative
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
function decodeServerEnvelope(text) {
|
|
2503
|
+
let value;
|
|
2504
|
+
try {
|
|
2505
|
+
value = JSON.parse(text);
|
|
2506
|
+
} catch {
|
|
2507
|
+
return invalid("message_malformed", "json_invalid");
|
|
2508
|
+
}
|
|
2509
|
+
if (!isRecord2(value)) {
|
|
2510
|
+
return invalid("message_malformed", "envelope_invalid");
|
|
2511
|
+
}
|
|
2512
|
+
if (value.envelopeVersion !== ENVELOPE_VERSION) {
|
|
2513
|
+
return invalid("envelope_unsupported", "envelope_version_unsupported");
|
|
2514
|
+
}
|
|
2515
|
+
if (value.a2uiVersion !== A2UI_PROTOCOL_VERSION || !isPositiveInteger(value.seq)) {
|
|
2516
|
+
return invalid("envelope_unsupported", "protocol_unsupported");
|
|
2517
|
+
}
|
|
2518
|
+
if (value.kind === "surfaceUpdate" || value.kind === "snapshot") {
|
|
2519
|
+
return decodeSurfaceEnvelope(value, value.kind);
|
|
2520
|
+
}
|
|
2521
|
+
if (value.kind === "ack") {
|
|
2522
|
+
return decodeAckEnvelope(value);
|
|
2523
|
+
}
|
|
2524
|
+
return invalid("envelope_unsupported", "kind_unsupported");
|
|
2525
|
+
}
|
|
2526
|
+
function decodeSurfaceEnvelope(value, kind) {
|
|
2527
|
+
const allowed = [
|
|
2528
|
+
"envelopeVersion",
|
|
2529
|
+
"a2uiVersion",
|
|
2530
|
+
"kind",
|
|
2531
|
+
"seq",
|
|
2532
|
+
"surfaceId",
|
|
2533
|
+
"revision",
|
|
2534
|
+
"activeStateId",
|
|
2535
|
+
"sectionNavigation",
|
|
2536
|
+
"fieldInteractions",
|
|
2537
|
+
"messages",
|
|
2538
|
+
...kind === "snapshot" ? ["inResponseTo"] : []
|
|
2539
|
+
];
|
|
2540
|
+
const required = [
|
|
2541
|
+
"envelopeVersion",
|
|
2542
|
+
"a2uiVersion",
|
|
2543
|
+
"kind",
|
|
2544
|
+
"seq",
|
|
2545
|
+
"surfaceId",
|
|
2546
|
+
"revision",
|
|
2547
|
+
"messages"
|
|
2548
|
+
];
|
|
2549
|
+
if (!hasExactKeys(value, allowed, required)) {
|
|
2550
|
+
return invalid("envelope_unsupported", "envelope_properties_invalid");
|
|
2551
|
+
}
|
|
2552
|
+
if (!isNonEmptyString2(value.surfaceId) || !isPositiveInteger(value.revision) || "activeStateId" in value && value.activeStateId !== null && !isNonEmptyString2(value.activeStateId) || "inResponseTo" in value && !isNonEmptyString2(value.inResponseTo) || "sectionNavigation" in value && !isSectionNavigation(value.sectionNavigation) || "fieldInteractions" in value && !isFieldInteractions(
|
|
2553
|
+
value.fieldInteractions,
|
|
2554
|
+
value.activeStateId,
|
|
2555
|
+
value.sectionNavigation
|
|
2556
|
+
)) {
|
|
2557
|
+
return invalid("message_malformed", "surface_metadata_invalid");
|
|
2558
|
+
}
|
|
2559
|
+
const parsedMessages = A2uiMessageListSchema2.safeParse(value.messages);
|
|
2560
|
+
if (!parsedMessages.success || parsedMessages.data.length === 0) {
|
|
2561
|
+
return invalid("message_malformed", "messages_invalid");
|
|
2562
|
+
}
|
|
2563
|
+
if (parsedMessages.data.some((message) => {
|
|
2564
|
+
const lifecycle = [
|
|
2565
|
+
"createSurface",
|
|
2566
|
+
"updateComponents",
|
|
2567
|
+
"updateDataModel",
|
|
2568
|
+
"deleteSurface"
|
|
2569
|
+
].find((key) => key in message);
|
|
2570
|
+
if (lifecycle === void 0) {
|
|
2571
|
+
return true;
|
|
2572
|
+
}
|
|
2573
|
+
const body = message[lifecycle];
|
|
2574
|
+
return body.surfaceId !== value.surfaceId;
|
|
2575
|
+
})) {
|
|
2576
|
+
return invalid("message_malformed", "message_surface_mismatch");
|
|
2577
|
+
}
|
|
2578
|
+
return {
|
|
2579
|
+
ok: true,
|
|
2580
|
+
envelope: value
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
var SERVER_REASON_CODES = /* @__PURE__ */ new Set([
|
|
2584
|
+
"envelope_version_unsupported",
|
|
2585
|
+
"a2ui_version_unsupported",
|
|
2586
|
+
"catalog_unsupported",
|
|
2587
|
+
"role_unauthorized",
|
|
2588
|
+
"surface_unknown",
|
|
2589
|
+
"state_mismatch",
|
|
2590
|
+
"revision_stale",
|
|
2591
|
+
"request_duplicate",
|
|
2592
|
+
"action_disallowed",
|
|
2593
|
+
"field_unknown",
|
|
2594
|
+
"value_invalid",
|
|
2595
|
+
"payload_malformed",
|
|
2596
|
+
"envelope_too_large"
|
|
2597
|
+
]);
|
|
2598
|
+
function decodeAckEnvelope(value) {
|
|
2599
|
+
const allowed = [
|
|
2600
|
+
"envelopeVersion",
|
|
2601
|
+
"a2uiVersion",
|
|
2602
|
+
"kind",
|
|
2603
|
+
"seq",
|
|
2604
|
+
"inResponseTo",
|
|
2605
|
+
"status",
|
|
2606
|
+
"reasonCode",
|
|
2607
|
+
"fieldErrors",
|
|
2608
|
+
"noSurface"
|
|
2609
|
+
];
|
|
2610
|
+
const required = [
|
|
2611
|
+
"envelopeVersion",
|
|
2612
|
+
"a2uiVersion",
|
|
2613
|
+
"kind",
|
|
2614
|
+
"seq",
|
|
2615
|
+
"inResponseTo",
|
|
2616
|
+
"status"
|
|
2617
|
+
];
|
|
2618
|
+
if (!hasExactKeys(value, allowed, required)) {
|
|
2619
|
+
return invalid("envelope_unsupported", "envelope_properties_invalid");
|
|
2620
|
+
}
|
|
2621
|
+
if (!isNonEmptyString2(value.inResponseTo) || value.status !== "accepted" && value.status !== "rejected" || value.status === "accepted" && "reasonCode" in value || value.status === "rejected" && (!isNonEmptyString2(value.reasonCode) || !SERVER_REASON_CODES.has(value.reasonCode)) || "noSurface" in value && typeof value.noSurface !== "boolean" || "fieldErrors" in value && !isFieldErrors(value.fieldErrors)) {
|
|
2622
|
+
return invalid("message_malformed", "ack_invalid");
|
|
2623
|
+
}
|
|
2624
|
+
return {
|
|
2625
|
+
ok: true,
|
|
2626
|
+
envelope: value
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
function isFieldErrors(value) {
|
|
2630
|
+
return Array.isArray(value) && value.every(
|
|
2631
|
+
(entry) => isRecord2(entry) && hasExactKeys(entry, ["field", "reasonCode"], ["field", "reasonCode"]) && isNonEmptyString2(entry.field) && isNonEmptyString2(entry.reasonCode) && SERVER_REASON_CODES.has(entry.reasonCode)
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
function isSectionNavigation(value) {
|
|
2635
|
+
if (!isRecord2(value) || !hasExactKeys(
|
|
2636
|
+
value,
|
|
2637
|
+
[
|
|
2638
|
+
"orderedSectionIds",
|
|
2639
|
+
"activeSectionId",
|
|
2640
|
+
"completedSectionIds",
|
|
2641
|
+
"futureSectionIds"
|
|
2642
|
+
],
|
|
2643
|
+
[
|
|
2644
|
+
"orderedSectionIds",
|
|
2645
|
+
"activeSectionId",
|
|
2646
|
+
"completedSectionIds",
|
|
2647
|
+
"futureSectionIds"
|
|
2648
|
+
]
|
|
2649
|
+
) || !isNonEmptyStringArray(value.orderedSectionIds) || value.orderedSectionIds.length === 0 || new Set(value.orderedSectionIds).size !== value.orderedSectionIds.length || value.activeSectionId !== null && !isNonEmptyString2(value.activeSectionId) || !isNonEmptyStringArray(value.completedSectionIds) || !isNonEmptyStringArray(value.futureSectionIds) || new Set(value.completedSectionIds).size !== value.completedSectionIds.length || new Set(value.futureSectionIds).size !== value.futureSectionIds.length) {
|
|
2650
|
+
return false;
|
|
2651
|
+
}
|
|
2652
|
+
const orderedSectionIds = value.orderedSectionIds;
|
|
2653
|
+
const completedSectionIds = value.completedSectionIds;
|
|
2654
|
+
const futureSectionIds = value.futureSectionIds;
|
|
2655
|
+
const activeSectionId = value.activeSectionId;
|
|
2656
|
+
const classified = [
|
|
2657
|
+
...completedSectionIds,
|
|
2658
|
+
...futureSectionIds,
|
|
2659
|
+
...activeSectionId === null ? [] : [activeSectionId]
|
|
2660
|
+
];
|
|
2661
|
+
return new Set(classified).size === classified.length && classified.length === orderedSectionIds.length && classified.every(
|
|
2662
|
+
(sectionId) => orderedSectionIds.includes(sectionId)
|
|
2663
|
+
) && arraysEqual(
|
|
2664
|
+
completedSectionIds,
|
|
2665
|
+
orderedSectionIds.filter(
|
|
2666
|
+
(sectionId) => completedSectionIds.includes(sectionId)
|
|
2667
|
+
)
|
|
2668
|
+
) && arraysEqual(
|
|
2669
|
+
futureSectionIds,
|
|
2670
|
+
orderedSectionIds.filter(
|
|
2671
|
+
(sectionId) => futureSectionIds.includes(sectionId)
|
|
2672
|
+
)
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
function isFieldInteractions(value, activeStateId, navigationValue) {
|
|
2676
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2677
|
+
return false;
|
|
2678
|
+
}
|
|
2679
|
+
const navigation = isSectionNavigation(navigationValue) ? navigationValue : null;
|
|
2680
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
2681
|
+
const fields = /* @__PURE__ */ new Set();
|
|
2682
|
+
const componentIds = /* @__PURE__ */ new Set();
|
|
2683
|
+
let currentCount = 0;
|
|
2684
|
+
for (const group of value) {
|
|
2685
|
+
if (!isRecord2(group) || !hasExactKeys(
|
|
2686
|
+
group,
|
|
2687
|
+
["groupId", "ownerStateId", "sectionId", "mode", "fields"],
|
|
2688
|
+
["groupId", "ownerStateId", "mode", "fields"]
|
|
2689
|
+
) || !isNonEmptyString2(group.groupId) || !isNonEmptyString2(group.ownerStateId) || group.mode !== "current" && group.mode !== "revision" || !Array.isArray(group.fields) || group.fields.length === 0 || groupIds.has(group.groupId)) {
|
|
2690
|
+
return false;
|
|
2691
|
+
}
|
|
2692
|
+
groupIds.add(group.groupId);
|
|
2693
|
+
if (group.mode === "current") {
|
|
2694
|
+
currentCount += 1;
|
|
2695
|
+
if (group.ownerStateId !== activeStateId) {
|
|
2696
|
+
return false;
|
|
2697
|
+
}
|
|
2698
|
+
} else if (group.ownerStateId === activeStateId) {
|
|
2699
|
+
return false;
|
|
2700
|
+
}
|
|
2701
|
+
if ("sectionId" in group) {
|
|
2702
|
+
if (!isNonEmptyString2(group.sectionId) || navigation === null || !navigation.orderedSectionIds.includes(group.sectionId) || group.mode === "current" && group.sectionId !== navigation.activeSectionId || group.mode === "revision" && group.sectionId !== navigation.activeSectionId && !navigation.completedSectionIds.includes(group.sectionId)) {
|
|
2703
|
+
return false;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
for (const binding of group.fields) {
|
|
2707
|
+
if (!isRecord2(binding) || !hasExactKeys(
|
|
2708
|
+
binding,
|
|
2709
|
+
["field", "componentId"],
|
|
2710
|
+
["field", "componentId"]
|
|
2711
|
+
) || !isNonEmptyString2(binding.field) || !isNonEmptyString2(binding.componentId) || fields.has(binding.field) || componentIds.has(binding.componentId)) {
|
|
2712
|
+
return false;
|
|
2713
|
+
}
|
|
2714
|
+
fields.add(binding.field);
|
|
2715
|
+
componentIds.add(binding.componentId);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return currentCount <= 1;
|
|
2719
|
+
}
|
|
2720
|
+
function isNonEmptyStringArray(value) {
|
|
2721
|
+
return Array.isArray(value) && value.every(isNonEmptyString2);
|
|
2722
|
+
}
|
|
2723
|
+
function arraysEqual(left, right) {
|
|
2724
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
2725
|
+
}
|
|
2726
|
+
function fieldValuesEqual(left, right, fields) {
|
|
2727
|
+
return fields.every(
|
|
2728
|
+
(field) => Object.hasOwn(left, field) && Object.hasOwn(right, field) && Object.is(left[field], right[field])
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
function isFieldValue3(value) {
|
|
2732
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
2733
|
+
}
|
|
2734
|
+
function isIncompleteRequiredValue(value) {
|
|
2735
|
+
return value === null || typeof value === "string" && value.trim() === "";
|
|
2736
|
+
}
|
|
2737
|
+
function envelopeRootFieldValues(envelope) {
|
|
2738
|
+
let values = null;
|
|
2739
|
+
for (const message of envelope.messages) {
|
|
2740
|
+
if (!isRecord2(message) || !isRecord2(message.updateDataModel)) {
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
const update = message.updateDataModel;
|
|
2744
|
+
if (update.surfaceId !== envelope.surfaceId || update.path !== void 0 && update.path !== "/" || !isRecord2(update.value)) {
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
const candidate = {};
|
|
2748
|
+
for (const [field, value] of Object.entries(update.value)) {
|
|
2749
|
+
if (isFieldValue3(value)) {
|
|
2750
|
+
candidate[field] = value;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
values = candidate;
|
|
2754
|
+
}
|
|
2755
|
+
return values;
|
|
2756
|
+
}
|
|
2757
|
+
function selectFieldValues(source, fields) {
|
|
2758
|
+
if (source === null) {
|
|
2759
|
+
return null;
|
|
2760
|
+
}
|
|
2761
|
+
const selected = {};
|
|
2762
|
+
for (const field of fields) {
|
|
2763
|
+
if (!Object.hasOwn(source, field)) {
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
selected[field] = source[field] ?? null;
|
|
2767
|
+
}
|
|
2768
|
+
return selected;
|
|
2769
|
+
}
|
|
2770
|
+
function hasExactKeys(value, allowed, required) {
|
|
2771
|
+
const keys = Object.keys(value);
|
|
2772
|
+
return keys.every((key) => allowed.includes(key)) && required.every((key) => Object.hasOwn(value, key));
|
|
2773
|
+
}
|
|
2774
|
+
function isRecord2(value) {
|
|
2775
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2776
|
+
}
|
|
2777
|
+
function isNonEmptyString2(value) {
|
|
2778
|
+
return typeof value === "string" && value.length > 0;
|
|
2779
|
+
}
|
|
2780
|
+
function isPositiveInteger(value) {
|
|
2781
|
+
return Number.isInteger(value) && typeof value === "number" && value > 0;
|
|
2782
|
+
}
|
|
2783
|
+
function invalid(errorCode, detail) {
|
|
2784
|
+
return { ok: false, errorCode, detail };
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/join.ts
|
|
2788
|
+
async function joinA2UIRoom(tokenProvider) {
|
|
2789
|
+
let material;
|
|
2790
|
+
try {
|
|
2791
|
+
material = await tokenProvider();
|
|
2792
|
+
} catch {
|
|
2793
|
+
throw new Error("A2UI room runtime material is unavailable.");
|
|
2794
|
+
}
|
|
2795
|
+
if (!isValidRuntimeMaterial(material)) {
|
|
2796
|
+
throw new Error("A2UI room runtime material is invalid.");
|
|
2797
|
+
}
|
|
2798
|
+
let room;
|
|
2799
|
+
try {
|
|
2800
|
+
const { Room } = await import("livekit-client");
|
|
2801
|
+
room = new Room();
|
|
2802
|
+
} catch {
|
|
2803
|
+
throw new Error("Unable to initialize the A2UI room.");
|
|
2804
|
+
}
|
|
2805
|
+
try {
|
|
2806
|
+
await room.connect(material.serverUrl, material.token);
|
|
2807
|
+
} catch {
|
|
2808
|
+
try {
|
|
2809
|
+
await room.disconnect();
|
|
2810
|
+
} catch {
|
|
2811
|
+
}
|
|
2812
|
+
throw new Error("Unable to join the A2UI room.");
|
|
2813
|
+
}
|
|
2814
|
+
let closePromise;
|
|
2815
|
+
return {
|
|
2816
|
+
room,
|
|
2817
|
+
close() {
|
|
2818
|
+
closePromise ??= disconnectWithoutLeaking(room);
|
|
2819
|
+
return closePromise;
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function isValidRuntimeMaterial(material) {
|
|
2824
|
+
if (typeof material !== "object" || material === null || typeof material.serverUrl !== "string" || material.serverUrl.length === 0 || typeof material.token !== "string" || material.token.length === 0) {
|
|
2825
|
+
return false;
|
|
2826
|
+
}
|
|
2827
|
+
try {
|
|
2828
|
+
const protocol = new URL(material.serverUrl).protocol;
|
|
2829
|
+
return ["wss:", "ws:", "https:", "http:"].includes(protocol);
|
|
2830
|
+
} catch {
|
|
2831
|
+
return false;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
async function disconnectWithoutLeaking(room) {
|
|
2835
|
+
try {
|
|
2836
|
+
await room.disconnect();
|
|
2837
|
+
} catch {
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
export {
|
|
2841
|
+
A2UIClientAdapter,
|
|
2842
|
+
A2UISessionController,
|
|
2843
|
+
A2UISurfaceHost,
|
|
2844
|
+
A2UI_CAPABILITIES_TOPIC,
|
|
2845
|
+
A2UI_CLIENT_TOPIC,
|
|
2846
|
+
A2UI_PROTOCOL_VERSION,
|
|
2847
|
+
A2UI_SERVER_TOPIC,
|
|
2848
|
+
CLIENT_ACTIONS,
|
|
2849
|
+
CLIENT_ERROR_CODES,
|
|
2850
|
+
ENVELOPE_VERSION,
|
|
2851
|
+
MAX_ENVELOPE_BYTES,
|
|
2852
|
+
joinA2UIRoom
|
|
2853
|
+
};
|
|
2854
|
+
//# sourceMappingURL=index.js.map
|