@dusted/anqst 1.5.0 → 1.6.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 +45 -4
- package/dist/src/app.js +78 -24
- package/dist/src/base93.js +0 -72
- package/dist/src/boundary-codec-analysis.js +468 -0
- package/dist/src/boundary-codec-leaves.js +602 -0
- package/dist/src/boundary-codec-model.js +77 -0
- package/dist/src/boundary-codec-plan.js +522 -0
- package/dist/src/boundary-codec-render.js +1738 -0
- package/dist/src/boundary-codecs.js +174 -0
- package/dist/src/emit.js +1960 -207
- package/dist/src/layout.js +9 -3
- package/dist/src/program.js +1 -1
- package/dist/src/project.js +3 -3
- package/package.json +2 -2
- package/spec/AnQst-Spec-DSL.d.ts +22 -24
- package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/decoder.js +0 -35
- package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/encoder.js +0 -36
- package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/decoder.js +0 -26
- package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/encoder.js +0 -38
- package/dist/src/codecgenerators/basecodecemitters/binary-blob/decoder.js +0 -28
- package/dist/src/codecgenerators/basecodecemitters/binary-blob/encoder.js +0 -34
- package/dist/src/codecgenerators/basecodecemitters/binary-buffer/decoder.js +0 -29
- package/dist/src/codecgenerators/basecodecemitters/binary-buffer/encoder.js +0 -36
- package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/decoder.js +0 -46
- package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/encoder.js +0 -49
- package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/decoder.js +0 -46
- package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/encoder.js +0 -47
- package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/decoder.js +0 -46
- package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/encoder.js +0 -49
- package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/decoder.js +0 -50
- package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/encoder.js +0 -52
- package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/decoder.js +0 -38
- package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/encoder.js +0 -44
- package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/decoder.js +0 -33
- package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/encoder.js +0 -34
- package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/decoder.js +0 -46
- package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/encoder.js +0 -49
- package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/decoder.js +0 -46
- package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/encoder.js +0 -49
- package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/decoder.js +0 -28
- package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/encoder.js +0 -34
- package/dist/src/codecgenerators/basecodecemitters/boolean/decoder.js +0 -34
- package/dist/src/codecgenerators/basecodecemitters/boolean/encoder.js +0 -40
- package/dist/src/codecgenerators/basecodecemitters/dynamic-json/decoder.js +0 -43
- package/dist/src/codecgenerators/basecodecemitters/dynamic-json/encoder.js +0 -45
- package/dist/src/codecgenerators/basecodecemitters/dynamic-object/decoder.js +0 -44
- package/dist/src/codecgenerators/basecodecemitters/dynamic-object/encoder.js +0 -46
- package/dist/src/codecgenerators/basecodecemitters/integer-int16/decoder.js +0 -32
- package/dist/src/codecgenerators/basecodecemitters/integer-int16/encoder.js +0 -43
- package/dist/src/codecgenerators/basecodecemitters/integer-int32/decoder.js +0 -26
- package/dist/src/codecgenerators/basecodecemitters/integer-int32/encoder.js +0 -37
- package/dist/src/codecgenerators/basecodecemitters/integer-int8/decoder.js +0 -26
- package/dist/src/codecgenerators/basecodecemitters/integer-int8/encoder.js +0 -37
- package/dist/src/codecgenerators/basecodecemitters/integer-qint16/decoder.js +0 -36
- package/dist/src/codecgenerators/basecodecemitters/integer-qint16/encoder.js +0 -36
- package/dist/src/codecgenerators/basecodecemitters/integer-qint32/decoder.js +0 -25
- package/dist/src/codecgenerators/basecodecemitters/integer-qint32/encoder.js +0 -36
- package/dist/src/codecgenerators/basecodecemitters/integer-qint8/decoder.js +0 -36
- package/dist/src/codecgenerators/basecodecemitters/integer-qint8/encoder.js +0 -36
- package/dist/src/codecgenerators/basecodecemitters/integer-quint16/decoder.js +0 -26
- package/dist/src/codecgenerators/basecodecemitters/integer-quint16/encoder.js +0 -38
- package/dist/src/codecgenerators/basecodecemitters/integer-quint32/decoder.js +0 -27
- package/dist/src/codecgenerators/basecodecemitters/integer-quint32/encoder.js +0 -39
- package/dist/src/codecgenerators/basecodecemitters/integer-quint8/decoder.js +0 -26
- package/dist/src/codecgenerators/basecodecemitters/integer-quint8/encoder.js +0 -38
- package/dist/src/codecgenerators/basecodecemitters/integer-uint16/decoder.js +0 -30
- package/dist/src/codecgenerators/basecodecemitters/integer-uint16/encoder.js +0 -42
- package/dist/src/codecgenerators/basecodecemitters/integer-uint32/decoder.js +0 -31
- package/dist/src/codecgenerators/basecodecemitters/integer-uint32/encoder.js +0 -43
- package/dist/src/codecgenerators/basecodecemitters/integer-uint8/decoder.js +0 -30
- package/dist/src/codecgenerators/basecodecemitters/integer-uint8/encoder.js +0 -40
- package/dist/src/codecgenerators/basecodecemitters/number/decoder.js +0 -26
- package/dist/src/codecgenerators/basecodecemitters/number/encoder.js +0 -38
- package/dist/src/codecgenerators/basecodecemitters/shared/comments.js +0 -13
- package/dist/src/codecgenerators/basecodecemitters/shared/contracts.js +0 -2
- package/dist/src/codecgenerators/basecodecemitters/shared/fixedwidth.js +0 -53
- package/dist/src/codecgenerators/basecodecemitters/shared/index.js +0 -21
- package/dist/src/codecgenerators/basecodecemitters/shared/positionalBase93.js +0 -48
- package/dist/src/codecgenerators/basecodecemitters/shared/rawbytes.js +0 -30
- package/dist/src/codecgenerators/basecodecemitters/string/decoder.js +0 -43
- package/dist/src/codecgenerators/basecodecemitters/string/encoder.js +0 -43
- package/dist/src/codecgenerators/basecodecemitters/stringArray/decoder.js +0 -80
- package/dist/src/codecgenerators/basecodecemitters/stringArray/encoder.js +0 -57
- package/dist/src/structured-top-level-codecs.js +0 -1305
package/dist/src/emit.js
CHANGED
|
@@ -13,7 +13,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
13
13
|
const typescript_1 = __importDefault(require("typescript"));
|
|
14
14
|
const pngjs_1 = require("pngjs");
|
|
15
15
|
const layout_1 = require("./layout");
|
|
16
|
-
const
|
|
16
|
+
const boundary_codecs_1 = require("./boundary-codecs");
|
|
17
17
|
function stripAnQstType(typeText) {
|
|
18
18
|
return typeText
|
|
19
19
|
.replace(/\bAnQst\.Type\.stringArray\b/g, "string[]")
|
|
@@ -83,6 +83,99 @@ function isNumberLikeUnionTypeNode(node) {
|
|
|
83
83
|
return false;
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
|
+
function collectFiniteStringLiteralsTypeNode(node) {
|
|
87
|
+
const values = [];
|
|
88
|
+
for (const part of node.types) {
|
|
89
|
+
if (!typescript_1.default.isLiteralTypeNode(part) || !typescript_1.default.isStringLiteral(part.literal))
|
|
90
|
+
return null;
|
|
91
|
+
values.push(part.literal.text);
|
|
92
|
+
}
|
|
93
|
+
return values;
|
|
94
|
+
}
|
|
95
|
+
function collectFiniteBooleanLiteralsTypeNode(node) {
|
|
96
|
+
const values = [];
|
|
97
|
+
for (const part of node.types) {
|
|
98
|
+
if (!typescript_1.default.isLiteralTypeNode(part))
|
|
99
|
+
return null;
|
|
100
|
+
if (part.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword) {
|
|
101
|
+
values.push(true);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (part.literal.kind === typescript_1.default.SyntaxKind.FalseKeyword) {
|
|
105
|
+
values.push(false);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return values;
|
|
111
|
+
}
|
|
112
|
+
function collectFiniteNumberLiteralsTypeNode(node) {
|
|
113
|
+
const values = [];
|
|
114
|
+
for (const part of node.types) {
|
|
115
|
+
if (!typescript_1.default.isLiteralTypeNode(part) || !typescript_1.default.isNumericLiteral(part.literal))
|
|
116
|
+
return null;
|
|
117
|
+
values.push(Number(part.literal.text));
|
|
118
|
+
}
|
|
119
|
+
return values;
|
|
120
|
+
}
|
|
121
|
+
function finiteDomainSymbolForCpp(value) {
|
|
122
|
+
if (typeof value === "boolean")
|
|
123
|
+
return value ? "True" : "False";
|
|
124
|
+
if (typeof value === "number") {
|
|
125
|
+
const text = Number.isInteger(value) ? `${value}` : `${value}`.replace(/\./g, "_");
|
|
126
|
+
return sanitizeIdentifier(`Value_${text.replace(/-/g, "neg_")}`);
|
|
127
|
+
}
|
|
128
|
+
const direct = sanitizeIdentifier(value.trim());
|
|
129
|
+
return direct.length > 0 ? direct : "Value";
|
|
130
|
+
}
|
|
131
|
+
function buildCppFiniteDomain(primitive, values) {
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const variants = [];
|
|
134
|
+
for (const value of values) {
|
|
135
|
+
const key = `${typeof value}:${String(value)}`;
|
|
136
|
+
if (seen.has(key))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
variants.push({
|
|
140
|
+
code: variants.length,
|
|
141
|
+
symbolicName: finiteDomainSymbolForCpp(value),
|
|
142
|
+
value
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { primitive, variants };
|
|
146
|
+
}
|
|
147
|
+
function collectFiniteDomainFromTypeNode(typeNode) {
|
|
148
|
+
if (typescript_1.default.isParenthesizedTypeNode(typeNode)) {
|
|
149
|
+
return collectFiniteDomainFromTypeNode(typeNode.type);
|
|
150
|
+
}
|
|
151
|
+
if (typescript_1.default.isLiteralTypeNode(typeNode)) {
|
|
152
|
+
if (typescript_1.default.isStringLiteral(typeNode.literal)) {
|
|
153
|
+
return buildCppFiniteDomain("string", [typeNode.literal.text]);
|
|
154
|
+
}
|
|
155
|
+
if (typescript_1.default.isNumericLiteral(typeNode.literal)) {
|
|
156
|
+
return buildCppFiniteDomain("number", [Number(typeNode.literal.text)]);
|
|
157
|
+
}
|
|
158
|
+
if (typeNode.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword || typeNode.literal.kind === typescript_1.default.SyntaxKind.FalseKeyword) {
|
|
159
|
+
return buildCppFiniteDomain("boolean", [typeNode.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword]);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (!typescript_1.default.isUnionTypeNode(typeNode))
|
|
164
|
+
return null;
|
|
165
|
+
const filtered = filterNullishUnionTypeNodes(typeNode.types);
|
|
166
|
+
if (filtered.length !== typeNode.types.length)
|
|
167
|
+
return null;
|
|
168
|
+
const finiteStrings = collectFiniteStringLiteralsTypeNode(typeNode);
|
|
169
|
+
if (finiteStrings)
|
|
170
|
+
return buildCppFiniteDomain("string", finiteStrings);
|
|
171
|
+
const finiteBooleans = collectFiniteBooleanLiteralsTypeNode(typeNode);
|
|
172
|
+
if (finiteBooleans)
|
|
173
|
+
return buildCppFiniteDomain("boolean", finiteBooleans);
|
|
174
|
+
const finiteNumbers = collectFiniteNumberLiteralsTypeNode(typeNode);
|
|
175
|
+
if (finiteNumbers)
|
|
176
|
+
return buildCppFiniteDomain("number", finiteNumbers);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
86
179
|
function mapTsTypeToCpp(typeText) {
|
|
87
180
|
const raw = typeText.trim();
|
|
88
181
|
if (/\bAnQst\.Type\.qint64\b/.test(raw))
|
|
@@ -179,6 +272,57 @@ function callbackName(memberName) {
|
|
|
179
272
|
function pascalCase(value) {
|
|
180
273
|
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
181
274
|
}
|
|
275
|
+
function sanitizeIdentifier(value) {
|
|
276
|
+
const trimmed = value.replace(/[^A-Za-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
277
|
+
const withFallback = trimmed.length > 0 ? trimmed : "Codec";
|
|
278
|
+
return /^[0-9]/.test(withFallback) ? `T_${withFallback}` : withFallback;
|
|
279
|
+
}
|
|
280
|
+
/** Qualify generated struct/enum names in public widget headers so TUs that include multiple widgets
|
|
281
|
+
* (each with `using namespace WidgetName`) do not get ambiguous unqualified types (e.g. two `Magic`). */
|
|
282
|
+
const CPP_WIDGET_HEADER_PRIMITIVES = new Set([
|
|
283
|
+
"void",
|
|
284
|
+
"bool",
|
|
285
|
+
"double",
|
|
286
|
+
"float",
|
|
287
|
+
"QString",
|
|
288
|
+
"QByteArray",
|
|
289
|
+
"QVariantMap",
|
|
290
|
+
"QStringList",
|
|
291
|
+
"qint64",
|
|
292
|
+
"quint64",
|
|
293
|
+
"qint32",
|
|
294
|
+
"quint32",
|
|
295
|
+
"qint16",
|
|
296
|
+
"quint16",
|
|
297
|
+
"qint8",
|
|
298
|
+
"quint8",
|
|
299
|
+
"int8_t",
|
|
300
|
+
"uint8_t",
|
|
301
|
+
"int16_t",
|
|
302
|
+
"uint16_t",
|
|
303
|
+
"int32_t",
|
|
304
|
+
"uint32_t"
|
|
305
|
+
]);
|
|
306
|
+
function qualifyCppTypeForWidgetHeaderPublicApi(widgetName, cppType) {
|
|
307
|
+
const trimmed = cppType.trim();
|
|
308
|
+
const listInner = /^QList<(.+)>$/.exec(trimmed);
|
|
309
|
+
if (listInner) {
|
|
310
|
+
return `QList<${qualifyCppTypeForWidgetHeaderPublicApi(widgetName, listInner[1])}>`;
|
|
311
|
+
}
|
|
312
|
+
if (trimmed.includes("::")) {
|
|
313
|
+
return trimmed;
|
|
314
|
+
}
|
|
315
|
+
if (CPP_WIDGET_HEADER_PRIMITIVES.has(trimmed)) {
|
|
316
|
+
return trimmed;
|
|
317
|
+
}
|
|
318
|
+
if (trimmed.startsWith("std::")) {
|
|
319
|
+
return trimmed;
|
|
320
|
+
}
|
|
321
|
+
if (/^(qint|quint)[0-9]{1,2}$/.test(trimmed)) {
|
|
322
|
+
return trimmed;
|
|
323
|
+
}
|
|
324
|
+
return `${widgetName}::${trimmed}`;
|
|
325
|
+
}
|
|
182
326
|
function variantToCppExpression(cppType, expr) {
|
|
183
327
|
if (cppType === "QString")
|
|
184
328
|
return `${expr}.toString()`;
|
|
@@ -471,6 +615,7 @@ class CppTypeNormalizer {
|
|
|
471
615
|
this.allKnownNames = new Set();
|
|
472
616
|
this.usedNames = new Set();
|
|
473
617
|
this.syntheticNameByKey = new Map();
|
|
618
|
+
this.finiteDomainNameByKey = new Map();
|
|
474
619
|
for (const decl of collectStructDecls(spec)) {
|
|
475
620
|
this.allKnownNames.add(decl.name);
|
|
476
621
|
this.usedNames.add(decl.name);
|
|
@@ -494,9 +639,11 @@ class CppTypeNormalizer {
|
|
|
494
639
|
const order = this.topologicalOrder();
|
|
495
640
|
const orderedDecls = order.map((name) => this.declMap.get(name)).filter((x) => !!x);
|
|
496
641
|
const structNames = orderedDecls.filter((d) => d.kind === "struct").map((d) => d.name);
|
|
642
|
+
const metatypeNames = orderedDecls.filter((d) => d.kind === "struct" || d.kind === "enum").map((d) => d.name);
|
|
497
643
|
return {
|
|
498
644
|
orderedDecls,
|
|
499
645
|
structNames,
|
|
646
|
+
metatypeNames,
|
|
500
647
|
mapTypeText: (typeText, nameHintParts) => this.mapTypeText(typeText, nameHintParts)
|
|
501
648
|
};
|
|
502
649
|
}
|
|
@@ -514,7 +661,19 @@ class CppTypeNormalizer {
|
|
|
514
661
|
optional: !!member.questionToken
|
|
515
662
|
});
|
|
516
663
|
}
|
|
517
|
-
return { name, kind: "struct", fields, aliasType: null, deps, isUnionAlias: false };
|
|
664
|
+
return { name, kind: "struct", fields, aliasType: null, deps, isUnionAlias: false, finiteDomain: null };
|
|
665
|
+
}
|
|
666
|
+
const finiteDomain = collectFiniteDomainFromTypeNode(node.type);
|
|
667
|
+
if (finiteDomain) {
|
|
668
|
+
return {
|
|
669
|
+
name,
|
|
670
|
+
kind: "enum",
|
|
671
|
+
fields: [],
|
|
672
|
+
aliasType: null,
|
|
673
|
+
deps: new Set(),
|
|
674
|
+
isUnionAlias: false,
|
|
675
|
+
finiteDomain
|
|
676
|
+
};
|
|
518
677
|
}
|
|
519
678
|
const deps = new Set();
|
|
520
679
|
const aliasType = this.mapTypeNode(node.type, [name], deps);
|
|
@@ -524,13 +683,18 @@ class CppTypeNormalizer {
|
|
|
524
683
|
fields: [],
|
|
525
684
|
aliasType,
|
|
526
685
|
deps,
|
|
527
|
-
isUnionAlias: node.type.getText().includes("|")
|
|
686
|
+
isUnionAlias: node.type.getText().includes("|"),
|
|
687
|
+
finiteDomain: null
|
|
528
688
|
};
|
|
529
689
|
}
|
|
530
690
|
mapTypeNode(typeNode, nameHintParts, deps) {
|
|
531
691
|
if (typescript_1.default.isParenthesizedTypeNode(typeNode)) {
|
|
532
692
|
return this.mapTypeNode(typeNode.type, nameHintParts, deps);
|
|
533
693
|
}
|
|
694
|
+
const finiteDomain = collectFiniteDomainFromTypeNode(typeNode);
|
|
695
|
+
if (finiteDomain) {
|
|
696
|
+
return this.ensureFiniteDomainType(finiteDomain, nameHintParts, deps);
|
|
697
|
+
}
|
|
534
698
|
if (typescript_1.default.isUnionTypeNode(typeNode)) {
|
|
535
699
|
const filtered = filterNullishUnionTypeNodes(typeNode.types);
|
|
536
700
|
if (filtered.length === 1) {
|
|
@@ -587,6 +751,34 @@ class CppTypeNormalizer {
|
|
|
587
751
|
this.collectKnownTypeDeps(mapped, deps);
|
|
588
752
|
return mapped;
|
|
589
753
|
}
|
|
754
|
+
ensureFiniteDomainType(domain, nameHintParts, deps) {
|
|
755
|
+
const baseName = this.makeSyntheticBaseName(nameHintParts);
|
|
756
|
+
const domainKey = `${baseName}::finite::${domain.primitive}::${domain.variants.map((variant) => `${variant.symbolicName}=${String(variant.value)}`).join("|")}`;
|
|
757
|
+
const existingName = this.finiteDomainNameByKey.get(domainKey);
|
|
758
|
+
if (existingName) {
|
|
759
|
+
deps.add(existingName);
|
|
760
|
+
return existingName;
|
|
761
|
+
}
|
|
762
|
+
const synthesizedName = this.allocateUniqueName(baseName);
|
|
763
|
+
this.finiteDomainNameByKey.set(domainKey, synthesizedName);
|
|
764
|
+
if (this.declMap.has(synthesizedName)) {
|
|
765
|
+
deps.add(synthesizedName);
|
|
766
|
+
return synthesizedName;
|
|
767
|
+
}
|
|
768
|
+
this.allKnownNames.add(synthesizedName);
|
|
769
|
+
this.declMap.set(synthesizedName, {
|
|
770
|
+
name: synthesizedName,
|
|
771
|
+
kind: "enum",
|
|
772
|
+
fields: [],
|
|
773
|
+
aliasType: null,
|
|
774
|
+
deps: new Set(),
|
|
775
|
+
isUnionAlias: false,
|
|
776
|
+
finiteDomain: domain
|
|
777
|
+
});
|
|
778
|
+
this.seedOrder.push(synthesizedName);
|
|
779
|
+
deps.add(synthesizedName);
|
|
780
|
+
return synthesizedName;
|
|
781
|
+
}
|
|
590
782
|
ensureSyntheticStruct(typeNode, nameHintParts, deps) {
|
|
591
783
|
const baseName = this.makeSyntheticBaseName(nameHintParts);
|
|
592
784
|
const syntheticKey = `${baseName}::${typeNode.getText()}`;
|
|
@@ -620,7 +812,8 @@ class CppTypeNormalizer {
|
|
|
620
812
|
fields,
|
|
621
813
|
aliasType: null,
|
|
622
814
|
deps: localDeps,
|
|
623
|
-
isUnionAlias: false
|
|
815
|
+
isUnionAlias: false,
|
|
816
|
+
finiteDomain: null
|
|
624
817
|
});
|
|
625
818
|
this.seedOrder.push(synthesizedName);
|
|
626
819
|
deps.add(synthesizedName);
|
|
@@ -682,6 +875,17 @@ class CppTypeNormalizer {
|
|
|
682
875
|
}
|
|
683
876
|
}
|
|
684
877
|
function renderCppDecl(decl) {
|
|
878
|
+
if (decl.kind === "enum") {
|
|
879
|
+
const variants = decl.finiteDomain?.variants ?? [];
|
|
880
|
+
const underlyingType = variants.length <= 0xff ? "std::uint8_t" : variants.length <= 0xffff ? "std::uint16_t" : "std::uint32_t";
|
|
881
|
+
const lines = [];
|
|
882
|
+
lines.push(`enum class ${decl.name} : ${underlyingType} {`);
|
|
883
|
+
for (const variant of variants) {
|
|
884
|
+
lines.push(` ${variant.symbolicName} = ${variant.code},`);
|
|
885
|
+
}
|
|
886
|
+
lines.push("};");
|
|
887
|
+
return lines.join("\n");
|
|
888
|
+
}
|
|
685
889
|
if (decl.kind === "alias") {
|
|
686
890
|
if (decl.isUnionAlias && decl.aliasType === "QString") {
|
|
687
891
|
return `using ${decl.name} = QString; // union mapped conservatively`;
|
|
@@ -737,6 +941,146 @@ function collectDragDropMimeConstants(spec) {
|
|
|
737
941
|
}
|
|
738
942
|
return constants;
|
|
739
943
|
}
|
|
944
|
+
function createCarrierSummary(counts, singleKinds = [], mayBlob = false, mustBlob = false) {
|
|
945
|
+
return {
|
|
946
|
+
counts: new Set(counts),
|
|
947
|
+
singleKinds: new Set(singleKinds),
|
|
948
|
+
mayBlob,
|
|
949
|
+
mustBlob
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function addOptionalAbsence(summary) {
|
|
953
|
+
const counts = new Set(summary.counts);
|
|
954
|
+
counts.add(0);
|
|
955
|
+
return {
|
|
956
|
+
counts,
|
|
957
|
+
singleKinds: new Set(summary.singleKinds),
|
|
958
|
+
mayBlob: summary.mayBlob,
|
|
959
|
+
mustBlob: summary.mustBlob
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function saturatingItemCountAdd(left, right) {
|
|
963
|
+
if (left === 2 || right === 2)
|
|
964
|
+
return 2;
|
|
965
|
+
const total = left + right;
|
|
966
|
+
return total >= 2 ? 2 : total;
|
|
967
|
+
}
|
|
968
|
+
function mergeCarrierSummaries(left, right) {
|
|
969
|
+
const counts = new Set();
|
|
970
|
+
const singleKinds = new Set();
|
|
971
|
+
for (const leftCount of left.counts) {
|
|
972
|
+
for (const rightCount of right.counts) {
|
|
973
|
+
const total = saturatingItemCountAdd(leftCount, rightCount);
|
|
974
|
+
counts.add(total);
|
|
975
|
+
if (total !== 1)
|
|
976
|
+
continue;
|
|
977
|
+
if (leftCount === 1 && rightCount === 0) {
|
|
978
|
+
for (const kind of left.singleKinds)
|
|
979
|
+
singleKinds.add(kind);
|
|
980
|
+
}
|
|
981
|
+
if (leftCount === 0 && rightCount === 1) {
|
|
982
|
+
for (const kind of right.singleKinds)
|
|
983
|
+
singleKinds.add(kind);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
counts,
|
|
989
|
+
singleKinds,
|
|
990
|
+
mayBlob: left.mayBlob || right.mayBlob,
|
|
991
|
+
mustBlob: left.mustBlob || right.mustBlob
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function summarizePlanCarrier(node) {
|
|
995
|
+
switch (node.nodeKind) {
|
|
996
|
+
case "leaf":
|
|
997
|
+
if (node.blobEntryId)
|
|
998
|
+
return createCarrierSummary([0], [], true, true);
|
|
999
|
+
if (node.itemEntryId) {
|
|
1000
|
+
return createCarrierSummary([1], [node.leaf.region === "dynamic" ? "object" : "string"]);
|
|
1001
|
+
}
|
|
1002
|
+
return createCarrierSummary([0]);
|
|
1003
|
+
case "named":
|
|
1004
|
+
return summarizePlanCarrier(node.target);
|
|
1005
|
+
case "finite-domain":
|
|
1006
|
+
if (node.blobEntryId)
|
|
1007
|
+
return createCarrierSummary([0], [], true, true);
|
|
1008
|
+
if (node.itemEntryId)
|
|
1009
|
+
return createCarrierSummary([1], ["string"]);
|
|
1010
|
+
return createCarrierSummary([0]);
|
|
1011
|
+
case "array": {
|
|
1012
|
+
if (node.extentStrategy === "blob-tail") {
|
|
1013
|
+
return createCarrierSummary([0], [], true, false);
|
|
1014
|
+
}
|
|
1015
|
+
const elementSummary = summarizePlanCarrier(node.element);
|
|
1016
|
+
const counts = new Set([0]);
|
|
1017
|
+
const singleKinds = new Set();
|
|
1018
|
+
if (elementSummary.counts.has(1)) {
|
|
1019
|
+
counts.add(1);
|
|
1020
|
+
for (const kind of elementSummary.singleKinds)
|
|
1021
|
+
singleKinds.add(kind);
|
|
1022
|
+
}
|
|
1023
|
+
if (elementSummary.counts.has(1) || elementSummary.counts.has(2)) {
|
|
1024
|
+
counts.add(2);
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
counts,
|
|
1028
|
+
singleKinds,
|
|
1029
|
+
mayBlob: true,
|
|
1030
|
+
mustBlob: true
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
case "struct": {
|
|
1034
|
+
let summary = createCarrierSummary([0]);
|
|
1035
|
+
for (const field of node.fields) {
|
|
1036
|
+
let fieldSummary = summarizePlanCarrier(field.node);
|
|
1037
|
+
if (field.optional) {
|
|
1038
|
+
fieldSummary = addOptionalAbsence(fieldSummary);
|
|
1039
|
+
}
|
|
1040
|
+
if (field.presenceStrategy) {
|
|
1041
|
+
fieldSummary = {
|
|
1042
|
+
counts: new Set(fieldSummary.counts),
|
|
1043
|
+
singleKinds: new Set(fieldSummary.singleKinds),
|
|
1044
|
+
mayBlob: true,
|
|
1045
|
+
mustBlob: true
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
summary = mergeCarrierSummaries(summary, fieldSummary);
|
|
1049
|
+
}
|
|
1050
|
+
return summary;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function inferDragDropCarrierKinds(plan) {
|
|
1055
|
+
const summary = summarizePlanCarrier(plan.root);
|
|
1056
|
+
const carriers = new Set();
|
|
1057
|
+
const addNoBlobCarriers = () => {
|
|
1058
|
+
if (summary.counts.has(0))
|
|
1059
|
+
carriers.add("array");
|
|
1060
|
+
if (summary.counts.has(1)) {
|
|
1061
|
+
for (const kind of summary.singleKinds)
|
|
1062
|
+
carriers.add(kind);
|
|
1063
|
+
}
|
|
1064
|
+
if (summary.counts.has(2))
|
|
1065
|
+
carriers.add("array");
|
|
1066
|
+
};
|
|
1067
|
+
if (summary.mustBlob) {
|
|
1068
|
+
if (summary.counts.has(0))
|
|
1069
|
+
carriers.add("string");
|
|
1070
|
+
if (summary.counts.has(1) || summary.counts.has(2))
|
|
1071
|
+
carriers.add("array");
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
addNoBlobCarriers();
|
|
1075
|
+
if (summary.mayBlob) {
|
|
1076
|
+
if (summary.counts.has(0))
|
|
1077
|
+
carriers.add("string");
|
|
1078
|
+
if (summary.counts.has(1) || summary.counts.has(2))
|
|
1079
|
+
carriers.add("array");
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return ["string", "array", "object"].filter((kind) => carriers.has(kind));
|
|
1083
|
+
}
|
|
740
1084
|
function collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog) {
|
|
741
1085
|
const seen = new Set();
|
|
742
1086
|
const helpers = [];
|
|
@@ -748,21 +1092,96 @@ function collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog) {
|
|
|
748
1092
|
if (seen.has(typeName))
|
|
749
1093
|
continue;
|
|
750
1094
|
seen.add(typeName);
|
|
751
|
-
const payloadSite = (0,
|
|
1095
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
752
1096
|
if (!payloadSite)
|
|
753
1097
|
continue;
|
|
1098
|
+
const plan = cppCodecCatalog.plansByCodecId.get(payloadSite.codecId);
|
|
1099
|
+
if (!plan)
|
|
1100
|
+
continue;
|
|
754
1101
|
helpers.push({
|
|
755
1102
|
typeName,
|
|
756
1103
|
cppType: cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]),
|
|
757
|
-
codecId: payloadSite.codecId
|
|
1104
|
+
codecId: payloadSite.codecId,
|
|
1105
|
+
carriers: inferDragDropCarrierKinds(plan)
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return helpers;
|
|
1110
|
+
}
|
|
1111
|
+
function collectTsDragDropPayloadHelpers(spec, codecCatalog) {
|
|
1112
|
+
const seen = new Set();
|
|
1113
|
+
const helpers = [];
|
|
1114
|
+
for (const service of spec.services) {
|
|
1115
|
+
for (const member of service.members) {
|
|
1116
|
+
if ((member.kind !== "DropTarget" && member.kind !== "HoverTarget") || !member.payloadTypeText)
|
|
1117
|
+
continue;
|
|
1118
|
+
const typeName = member.payloadTypeText.replace(/\s/g, "");
|
|
1119
|
+
if (seen.has(typeName))
|
|
1120
|
+
continue;
|
|
1121
|
+
seen.add(typeName);
|
|
1122
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
1123
|
+
if (!payloadSite)
|
|
1124
|
+
continue;
|
|
1125
|
+
const plan = codecCatalog.plansByCodecId.get(payloadSite.codecId);
|
|
1126
|
+
if (!plan)
|
|
1127
|
+
continue;
|
|
1128
|
+
helpers.push({
|
|
1129
|
+
typeName,
|
|
1130
|
+
tsType: mapTypeTextToTs(member.payloadTypeText),
|
|
1131
|
+
codecId: payloadSite.codecId,
|
|
1132
|
+
carriers: inferDragDropCarrierKinds(plan)
|
|
758
1133
|
});
|
|
759
1134
|
}
|
|
760
1135
|
}
|
|
761
1136
|
return helpers;
|
|
762
1137
|
}
|
|
1138
|
+
function renderTsDragDropPayloadHelpers(spec, codecCatalog) {
|
|
1139
|
+
const helpers = collectTsDragDropPayloadHelpers(spec, codecCatalog);
|
|
1140
|
+
return helpers
|
|
1141
|
+
.map((helper) => {
|
|
1142
|
+
const lines = [
|
|
1143
|
+
`function decodeDragDropPayload_${helper.typeName}(rawPayload: unknown): ${helper.tsType} {`,
|
|
1144
|
+
` if (typeof rawPayload !== "string") {`,
|
|
1145
|
+
` throw new Error("Drag/drop payload must be tagged text.");`,
|
|
1146
|
+
` }`,
|
|
1147
|
+
` if (rawPayload.length === 0) {`,
|
|
1148
|
+
` throw new Error("Drag/drop payload is empty.");`,
|
|
1149
|
+
` }`,
|
|
1150
|
+
` const transportTag = rawPayload[0];`,
|
|
1151
|
+
` const payloadText = rawPayload.slice(1);`
|
|
1152
|
+
];
|
|
1153
|
+
if (helper.carriers.includes("string")) {
|
|
1154
|
+
lines.push(` if (transportTag === "S") {`);
|
|
1155
|
+
lines.push(` return decode${helper.codecId}(payloadText);`);
|
|
1156
|
+
lines.push(` }`);
|
|
1157
|
+
}
|
|
1158
|
+
if (helper.carriers.includes("array")) {
|
|
1159
|
+
lines.push(` if (transportTag === "A") {`);
|
|
1160
|
+
lines.push(` const parsed = JSON.parse(payloadText) as unknown;`);
|
|
1161
|
+
lines.push(` if (!Array.isArray(parsed)) {`);
|
|
1162
|
+
lines.push(` throw new Error("Drag/drop payload must be a JSON array.");`);
|
|
1163
|
+
lines.push(` }`);
|
|
1164
|
+
lines.push(` return decode${helper.codecId}(parsed);`);
|
|
1165
|
+
lines.push(` }`);
|
|
1166
|
+
}
|
|
1167
|
+
if (helper.carriers.includes("object")) {
|
|
1168
|
+
lines.push(` if (transportTag === "O") {`);
|
|
1169
|
+
lines.push(` const parsed = JSON.parse(payloadText) as unknown;`);
|
|
1170
|
+
lines.push(` if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {`);
|
|
1171
|
+
lines.push(` throw new Error("Drag/drop payload must be a JSON object.");`);
|
|
1172
|
+
lines.push(` }`);
|
|
1173
|
+
lines.push(` return decode${helper.codecId}(parsed);`);
|
|
1174
|
+
lines.push(` }`);
|
|
1175
|
+
}
|
|
1176
|
+
lines.push(` throw new Error(\`Drag/drop payload has an unknown transport tag: \${transportTag}\`);`);
|
|
1177
|
+
lines.push(`}`);
|
|
1178
|
+
return lines.join("\n");
|
|
1179
|
+
})
|
|
1180
|
+
.join("\n\n");
|
|
1181
|
+
}
|
|
763
1182
|
function renderTypesHeader(spec, cppTypes) {
|
|
764
1183
|
const decls = cppTypes.orderedDecls.map(renderCppDecl).join("\n\n");
|
|
765
|
-
const metatypes = cppTypes.
|
|
1184
|
+
const metatypes = cppTypes.metatypeNames
|
|
766
1185
|
.flatMap((name) => [
|
|
767
1186
|
`Q_DECLARE_METATYPE(${spec.widgetName}::${name})`,
|
|
768
1187
|
`Q_DECLARE_METATYPE(QList<${spec.widgetName}::${name}>)`
|
|
@@ -794,14 +1213,12 @@ ${metatypes}
|
|
|
794
1213
|
}
|
|
795
1214
|
function renderWidgetUmbrellaHeader(spec) {
|
|
796
1215
|
return `#pragma once
|
|
797
|
-
// Built by <AnQst_version>
|
|
798
1216
|
#include "${spec.widgetName}Widget.h"
|
|
799
1217
|
#include "${spec.widgetName}Types.h"
|
|
800
1218
|
`;
|
|
801
1219
|
}
|
|
802
|
-
function renderWidgetHeader(spec, cppTypes) {
|
|
1220
|
+
function renderWidgetHeader(spec, cppTypes, cppCodecCatalog) {
|
|
803
1221
|
const widgetClassName = `${spec.widgetName}Widget`;
|
|
804
|
-
const cppCodecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
805
1222
|
const dragDropPayloadHelpers = collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog);
|
|
806
1223
|
const callbackAliases = [];
|
|
807
1224
|
const publicMethods = [];
|
|
@@ -812,34 +1229,44 @@ function renderWidgetHeader(spec, cppTypes) {
|
|
|
812
1229
|
const properties = [];
|
|
813
1230
|
const fields = [];
|
|
814
1231
|
const publicSlots = [];
|
|
815
|
-
const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) =>
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1232
|
+
const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) => {
|
|
1233
|
+
const qPayload = qualifyCppTypeForWidgetHeaderPublicApi(spec.widgetName, helper.cppType);
|
|
1234
|
+
return [
|
|
1235
|
+
`static QByteArray encodeDragDropPayload_${helper.typeName}(const ${qPayload}& payload);`,
|
|
1236
|
+
`static std::optional<${qPayload}> decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload);`
|
|
1237
|
+
];
|
|
1238
|
+
});
|
|
1239
|
+
const qType = (t) => qualifyCppTypeForWidgetHeaderPublicApi(spec.widgetName, t);
|
|
820
1240
|
for (const service of spec.services) {
|
|
821
1241
|
for (const member of service.members) {
|
|
822
|
-
bindings.push({ service: service.name, member: member.name, kind: member.kind });
|
|
823
1242
|
const memberPascal = pascalCase(member.name);
|
|
824
1243
|
if (member.kind === "Call" && member.payloadTypeText) {
|
|
825
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
826
|
-
const args = member.parameters
|
|
1244
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1245
|
+
const args = member.parameters
|
|
1246
|
+
.map((p) => `const ${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))}& ${p.name}`)
|
|
1247
|
+
.join(", ");
|
|
827
1248
|
callbackAliases.push(`using ${memberPascal}Handler = std::function<${cppType}(${args})>;`);
|
|
828
1249
|
handleMethods.push(` void ${member.name}(const ${memberPascal}Handler& handler) const;`);
|
|
829
1250
|
callSetterMethods.push(`void set${memberPascal}CallHandler(const ${memberPascal}Handler& handler);`);
|
|
830
1251
|
fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
|
|
831
1252
|
}
|
|
832
1253
|
else if (member.kind === "Emitter") {
|
|
833
|
-
const args = member.parameters
|
|
1254
|
+
const args = member.parameters
|
|
1255
|
+
.map((p) => `const ${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))}& ${p.name}`)
|
|
1256
|
+
.join(", ");
|
|
834
1257
|
signals.push(`void ${member.name}(${args});`);
|
|
835
1258
|
}
|
|
836
1259
|
else if (member.kind === "Slot") {
|
|
837
|
-
const ret = member.payloadTypeText
|
|
838
|
-
|
|
1260
|
+
const ret = member.payloadTypeText
|
|
1261
|
+
? qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]))
|
|
1262
|
+
: "void";
|
|
1263
|
+
const args = member.parameters
|
|
1264
|
+
.map((p) => `${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))} ${p.name}`)
|
|
1265
|
+
.join(", ");
|
|
839
1266
|
slotMethods.push(`${ret} slot_${member.name}(${args});`);
|
|
840
1267
|
}
|
|
841
1268
|
else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
|
|
842
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1269
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
843
1270
|
const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
|
|
844
1271
|
properties.push(`Q_PROPERTY(${cppType} ${member.name} READ ${member.name} WRITE set${cap} NOTIFY ${member.name}Changed)`);
|
|
845
1272
|
publicMethods.push(`${cppType} ${member.name}() const;`);
|
|
@@ -856,11 +1283,11 @@ function renderWidgetHeader(spec, cppTypes) {
|
|
|
856
1283
|
}
|
|
857
1284
|
}
|
|
858
1285
|
else if (member.kind === "DropTarget" && member.payloadTypeText) {
|
|
859
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1286
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
860
1287
|
signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
|
|
861
1288
|
}
|
|
862
1289
|
else if (member.kind === "HoverTarget" && member.payloadTypeText) {
|
|
863
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1290
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
864
1291
|
signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
|
|
865
1292
|
signals.push(`void ${member.name}Left();`);
|
|
866
1293
|
}
|
|
@@ -929,13 +1356,6 @@ private:
|
|
|
929
1356
|
QVariantList args;
|
|
930
1357
|
QDateTime enqueuedAt;
|
|
931
1358
|
};
|
|
932
|
-
struct BridgeBindingRow {
|
|
933
|
-
const char* service;
|
|
934
|
-
const char* member;
|
|
935
|
-
const char* kind;
|
|
936
|
-
};
|
|
937
|
-
static const BridgeBindingRow kBridgeBindings[];
|
|
938
|
-
static constexpr int kBridgeBindingsCount = ${bindings.length};
|
|
939
1359
|
static QString makeBindingKey(const QString& service, const QString& member);
|
|
940
1360
|
void installBridgeBindings();
|
|
941
1361
|
bool hasEmitterListeners(const QString& service, const QString& member) const;
|
|
@@ -957,18 +1377,19 @@ ${fields.map((f) => ` ${f}`).join("\n")}
|
|
|
957
1377
|
};
|
|
958
1378
|
`;
|
|
959
1379
|
}
|
|
960
|
-
function renderCppStub(spec, cppTypes) {
|
|
1380
|
+
function renderCppStub(spec, cppTypes, cppCodecCatalog) {
|
|
961
1381
|
const widgetClassName = `${spec.widgetName}Widget`;
|
|
962
|
-
const cppCodecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
963
1382
|
const dragDropPayloadHelpers = collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog);
|
|
964
|
-
const cppCodecHelpers = (0,
|
|
1383
|
+
const cppCodecHelpers = (0, boundary_codecs_1.renderCppBoundaryCodecHelpers)(cppCodecCatalog, (typeText, pathHintParts) => cppTypes.mapTypeText(typeText, pathHintParts)).trim();
|
|
965
1384
|
const lines = [];
|
|
966
1385
|
lines.push(`#include "include/${spec.widgetName}Widget.h"`);
|
|
1386
|
+
lines.push(`#include "AnQstBase93.h"`);
|
|
967
1387
|
lines.push(`#include <QDebug>`);
|
|
968
1388
|
lines.push(`#include <QElapsedTimer>`);
|
|
969
1389
|
lines.push(`#include <QEventLoop>`);
|
|
970
1390
|
lines.push(`#include <QJsonArray>`);
|
|
971
1391
|
lines.push(`#include <QJsonDocument>`);
|
|
1392
|
+
lines.push(`#include <QJsonObject>`);
|
|
972
1393
|
lines.push(`#include <QMetaType>`);
|
|
973
1394
|
lines.push(`#include <QTimer>`);
|
|
974
1395
|
lines.push(`#include <cstring>`);
|
|
@@ -984,7 +1405,7 @@ function renderCppStub(spec, cppTypes) {
|
|
|
984
1405
|
lines.push("namespace {");
|
|
985
1406
|
lines.push("void registerGeneratedMetaTypes() {");
|
|
986
1407
|
lines.push(" static const bool registered = []() {");
|
|
987
|
-
for (const typeName of cppTypes.
|
|
1408
|
+
for (const typeName of cppTypes.metatypeNames) {
|
|
988
1409
|
lines.push(` qRegisterMetaType<${spec.widgetName}::${typeName}>("${spec.widgetName}::${typeName}");`);
|
|
989
1410
|
lines.push(` qRegisterMetaType<QList<${spec.widgetName}::${typeName}>>("QList<${spec.widgetName}::${typeName}>");`);
|
|
990
1411
|
}
|
|
@@ -1000,20 +1421,78 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1000
1421
|
lines.push("");
|
|
1001
1422
|
for (const helper of dragDropPayloadHelpers) {
|
|
1002
1423
|
lines.push(`QByteArray ${widgetClassName}::encodeDragDropPayload_${helper.typeName}(const ${helper.cppType}& payload) {`);
|
|
1003
|
-
lines.push(`
|
|
1424
|
+
lines.push(` const QVariant wire = encode${helper.codecId}(payload);`);
|
|
1425
|
+
if (helper.carriers.includes("string")) {
|
|
1426
|
+
lines.push(` if (wire.type() == QVariant::String) {`);
|
|
1427
|
+
lines.push(` QByteArray out;`);
|
|
1428
|
+
lines.push(` out.append('S');`);
|
|
1429
|
+
lines.push(` out.append(wire.toString().toUtf8());`);
|
|
1430
|
+
lines.push(` return out;`);
|
|
1431
|
+
lines.push(` }`);
|
|
1432
|
+
}
|
|
1433
|
+
if (helper.carriers.includes("array")) {
|
|
1434
|
+
lines.push(` if (wire.type() == QVariant::List) {`);
|
|
1435
|
+
lines.push(` QByteArray out;`);
|
|
1436
|
+
lines.push(` out.append('A');`);
|
|
1437
|
+
lines.push(` out.append(QJsonDocument(QJsonArray::fromVariantList(wire.toList())).toJson(QJsonDocument::Compact));`);
|
|
1438
|
+
lines.push(` return out;`);
|
|
1439
|
+
lines.push(` }`);
|
|
1440
|
+
}
|
|
1441
|
+
if (helper.carriers.includes("object")) {
|
|
1442
|
+
lines.push(` if (wire.type() == QVariant::Map) {`);
|
|
1443
|
+
lines.push(` QByteArray out;`);
|
|
1444
|
+
lines.push(` out.append('O');`);
|
|
1445
|
+
lines.push(` out.append(QJsonDocument(QJsonObject::fromVariantMap(wire.toMap())).toJson(QJsonDocument::Compact));`);
|
|
1446
|
+
lines.push(` return out;`);
|
|
1447
|
+
lines.push(` }`);
|
|
1448
|
+
}
|
|
1449
|
+
lines.push(` throw std::runtime_error("AnQst drag/drop payload codec emitted an unsupported top-level carrier.");`);
|
|
1004
1450
|
lines.push(`}`);
|
|
1005
1451
|
lines.push("");
|
|
1006
1452
|
lines.push(`std::optional<${helper.cppType}> ${widgetClassName}::decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload) {`);
|
|
1007
|
-
lines.push(`
|
|
1008
|
-
lines.push(` const QJsonDocument document = QJsonDocument::fromJson(rawPayload, &parseError);`);
|
|
1009
|
-
lines.push(` if (parseError.error != QJsonParseError::NoError || !document.isArray()) {`);
|
|
1010
|
-
lines.push(` return std::nullopt;`);
|
|
1011
|
-
lines.push(` }`);
|
|
1012
|
-
lines.push(` try {`);
|
|
1013
|
-
lines.push(` return decode${helper.codecId}(document.array().toVariantList());`);
|
|
1014
|
-
lines.push(` } catch (...) {`);
|
|
1453
|
+
lines.push(` if (rawPayload.isEmpty()) {`);
|
|
1015
1454
|
lines.push(` return std::nullopt;`);
|
|
1016
1455
|
lines.push(` }`);
|
|
1456
|
+
lines.push(` const char transportTag = rawPayload.at(0);`);
|
|
1457
|
+
lines.push(` const QByteArray payloadBytes = rawPayload.mid(1);`);
|
|
1458
|
+
if (helper.carriers.includes("string")) {
|
|
1459
|
+
lines.push(` if (transportTag == 'S') {`);
|
|
1460
|
+
lines.push(` try {`);
|
|
1461
|
+
lines.push(` return decode${helper.codecId}(QString::fromUtf8(payloadBytes));`);
|
|
1462
|
+
lines.push(` } catch (...) {`);
|
|
1463
|
+
lines.push(` return std::nullopt;`);
|
|
1464
|
+
lines.push(` }`);
|
|
1465
|
+
lines.push(` }`);
|
|
1466
|
+
}
|
|
1467
|
+
if (helper.carriers.includes("array")) {
|
|
1468
|
+
lines.push(` if (transportTag == 'A') {`);
|
|
1469
|
+
lines.push(` QJsonParseError parseError;`);
|
|
1470
|
+
lines.push(` const QJsonDocument document = QJsonDocument::fromJson(payloadBytes, &parseError);`);
|
|
1471
|
+
lines.push(` if (parseError.error != QJsonParseError::NoError || !document.isArray()) {`);
|
|
1472
|
+
lines.push(` return std::nullopt;`);
|
|
1473
|
+
lines.push(` }`);
|
|
1474
|
+
lines.push(` try {`);
|
|
1475
|
+
lines.push(` return decode${helper.codecId}(QVariant(document.array().toVariantList()));`);
|
|
1476
|
+
lines.push(` } catch (...) {`);
|
|
1477
|
+
lines.push(` return std::nullopt;`);
|
|
1478
|
+
lines.push(` }`);
|
|
1479
|
+
lines.push(` }`);
|
|
1480
|
+
}
|
|
1481
|
+
if (helper.carriers.includes("object")) {
|
|
1482
|
+
lines.push(` if (transportTag == 'O') {`);
|
|
1483
|
+
lines.push(` QJsonParseError parseError;`);
|
|
1484
|
+
lines.push(` const QJsonDocument document = QJsonDocument::fromJson(payloadBytes, &parseError);`);
|
|
1485
|
+
lines.push(` if (parseError.error != QJsonParseError::NoError || !document.isObject()) {`);
|
|
1486
|
+
lines.push(` return std::nullopt;`);
|
|
1487
|
+
lines.push(` }`);
|
|
1488
|
+
lines.push(` try {`);
|
|
1489
|
+
lines.push(` return decode${helper.codecId}(QVariant(document.object().toVariantMap()));`);
|
|
1490
|
+
lines.push(` } catch (...) {`);
|
|
1491
|
+
lines.push(` return std::nullopt;`);
|
|
1492
|
+
lines.push(` }`);
|
|
1493
|
+
lines.push(` }`);
|
|
1494
|
+
}
|
|
1495
|
+
lines.push(` return std::nullopt;`);
|
|
1017
1496
|
lines.push(`}`);
|
|
1018
1497
|
lines.push("");
|
|
1019
1498
|
}
|
|
@@ -1033,14 +1512,6 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1033
1512
|
lines.push("");
|
|
1034
1513
|
}
|
|
1035
1514
|
}
|
|
1036
|
-
lines.push(`const ${widgetClassName}::BridgeBindingRow ${widgetClassName}::kBridgeBindings[] = {`);
|
|
1037
|
-
for (const service of spec.services) {
|
|
1038
|
-
for (const member of service.members) {
|
|
1039
|
-
lines.push(` {"${service.name}", "${member.name}", "${member.kind}"},`);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
lines.push(`};`);
|
|
1043
|
-
lines.push("");
|
|
1044
1515
|
lines.push(`${widgetClassName}::${widgetClassName}(QWidget* parent) : AnQstWebHostBase(parent), handle(this) {`);
|
|
1045
1516
|
lines.push(` static const bool kResourcesInitialized = []() {`);
|
|
1046
1517
|
lines.push(` ::qInitResources_${spec.widgetName}();`);
|
|
@@ -1067,19 +1538,89 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1067
1538
|
for (const member of service.members) {
|
|
1068
1539
|
if (member.kind === "DropTarget" && member.payloadTypeText) {
|
|
1069
1540
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1070
|
-
const payloadSite = (0,
|
|
1541
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1542
|
+
const typeName = member.payloadTypeText.replace(/\s/g, "");
|
|
1071
1543
|
lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_dropReceived, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
|
|
1072
1544
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
1073
|
-
|
|
1545
|
+
if (payloadSite) {
|
|
1546
|
+
lines.push(` if (payload.type() != QVariant::String) {`);
|
|
1547
|
+
lines.push(` emitHostError(`);
|
|
1548
|
+
lines.push(` QStringLiteral("DeserializationError"),`);
|
|
1549
|
+
lines.push(` QStringLiteral("bridge"),`);
|
|
1550
|
+
lines.push(` QStringLiteral("error"),`);
|
|
1551
|
+
lines.push(` true,`);
|
|
1552
|
+
lines.push(` QStringLiteral("Failed to deserialize DropTarget ${service.name}.${member.name}."),`);
|
|
1553
|
+
lines.push(` {`);
|
|
1554
|
+
lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
|
|
1555
|
+
lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
|
|
1556
|
+
lines.push(` {QStringLiteral("detail"), QStringLiteral("Host did not provide tagged drag/drop payload text.")},`);
|
|
1557
|
+
lines.push(` });`);
|
|
1558
|
+
lines.push(` return;`);
|
|
1559
|
+
lines.push(` }`);
|
|
1560
|
+
lines.push(` const auto decodedPayload = decodeDragDropPayload_${typeName}(payload.toString().toUtf8());`);
|
|
1561
|
+
lines.push(` if (!decodedPayload.has_value()) {`);
|
|
1562
|
+
lines.push(` emitHostError(`);
|
|
1563
|
+
lines.push(` QStringLiteral("DeserializationError"),`);
|
|
1564
|
+
lines.push(` QStringLiteral("bridge"),`);
|
|
1565
|
+
lines.push(` QStringLiteral("error"),`);
|
|
1566
|
+
lines.push(` true,`);
|
|
1567
|
+
lines.push(` QStringLiteral("Failed to deserialize DropTarget ${service.name}.${member.name}."),`);
|
|
1568
|
+
lines.push(` {`);
|
|
1569
|
+
lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
|
|
1570
|
+
lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
|
|
1571
|
+
lines.push(` {QStringLiteral("detail"), QStringLiteral("Tagged drag/drop payload did not match the planned boundary carrier.")},`);
|
|
1572
|
+
lines.push(` });`);
|
|
1573
|
+
lines.push(` return;`);
|
|
1574
|
+
lines.push(` }`);
|
|
1575
|
+
lines.push(` emit ${member.name}(*decodedPayload, x, y);`);
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
|
|
1579
|
+
}
|
|
1074
1580
|
lines.push(` }`);
|
|
1075
1581
|
lines.push(` });`);
|
|
1076
1582
|
}
|
|
1077
1583
|
else if (member.kind === "HoverTarget" && member.payloadTypeText) {
|
|
1078
1584
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1079
|
-
const payloadSite = (0,
|
|
1585
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1586
|
+
const typeName = member.payloadTypeText.replace(/\s/g, "");
|
|
1080
1587
|
lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverUpdated, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
|
|
1081
1588
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
1082
|
-
|
|
1589
|
+
if (payloadSite) {
|
|
1590
|
+
lines.push(` if (payload.type() != QVariant::String) {`);
|
|
1591
|
+
lines.push(` emitHostError(`);
|
|
1592
|
+
lines.push(` QStringLiteral("DeserializationError"),`);
|
|
1593
|
+
lines.push(` QStringLiteral("bridge"),`);
|
|
1594
|
+
lines.push(` QStringLiteral("error"),`);
|
|
1595
|
+
lines.push(` true,`);
|
|
1596
|
+
lines.push(` QStringLiteral("Failed to deserialize HoverTarget ${service.name}.${member.name}."),`);
|
|
1597
|
+
lines.push(` {`);
|
|
1598
|
+
lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
|
|
1599
|
+
lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
|
|
1600
|
+
lines.push(` {QStringLiteral("detail"), QStringLiteral("Host did not provide tagged drag/drop payload text.")},`);
|
|
1601
|
+
lines.push(` });`);
|
|
1602
|
+
lines.push(` return;`);
|
|
1603
|
+
lines.push(` }`);
|
|
1604
|
+
lines.push(` const auto decodedPayload = decodeDragDropPayload_${typeName}(payload.toString().toUtf8());`);
|
|
1605
|
+
lines.push(` if (!decodedPayload.has_value()) {`);
|
|
1606
|
+
lines.push(` emitHostError(`);
|
|
1607
|
+
lines.push(` QStringLiteral("DeserializationError"),`);
|
|
1608
|
+
lines.push(` QStringLiteral("bridge"),`);
|
|
1609
|
+
lines.push(` QStringLiteral("error"),`);
|
|
1610
|
+
lines.push(` true,`);
|
|
1611
|
+
lines.push(` QStringLiteral("Failed to deserialize HoverTarget ${service.name}.${member.name}."),`);
|
|
1612
|
+
lines.push(` {`);
|
|
1613
|
+
lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
|
|
1614
|
+
lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
|
|
1615
|
+
lines.push(` {QStringLiteral("detail"), QStringLiteral("Tagged drag/drop payload did not match the planned boundary carrier.")},`);
|
|
1616
|
+
lines.push(` });`);
|
|
1617
|
+
lines.push(` return;`);
|
|
1618
|
+
lines.push(` }`);
|
|
1619
|
+
lines.push(` emit ${member.name}(*decodedPayload, x, y);`);
|
|
1620
|
+
}
|
|
1621
|
+
else {
|
|
1622
|
+
lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
|
|
1623
|
+
}
|
|
1083
1624
|
lines.push(` }`);
|
|
1084
1625
|
lines.push(` });`);
|
|
1085
1626
|
lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverLeft, this, [this](const QString& service, const QString& member) {`);
|
|
@@ -1185,12 +1726,12 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1185
1726
|
continue;
|
|
1186
1727
|
const timeoutMs = member.timeoutMs;
|
|
1187
1728
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1188
|
-
const payloadSite = (0,
|
|
1729
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1189
1730
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
1190
1731
|
for (let i = 0; i < member.parameters.length; i++) {
|
|
1191
1732
|
const p = member.parameters[i];
|
|
1192
1733
|
const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
|
|
1193
|
-
const paramSite = (0,
|
|
1734
|
+
const paramSite = (0, boundary_codecs_1.getBoundaryParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
|
|
1194
1735
|
lines.push(` const ${pType} ${p.name} = ${paramSite ? `decode${paramSite.codecId}(args.value(${i}))` : variantToCppExpression(pType, `args.value(${i})`)};`);
|
|
1195
1736
|
}
|
|
1196
1737
|
lines.push(` const QString requestId = QStringLiteral("call-%1").arg(++m_callRequestCounter);`);
|
|
@@ -1260,7 +1801,7 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1260
1801
|
for (let i = 0; i < member.parameters.length; i++) {
|
|
1261
1802
|
const p = member.parameters[i];
|
|
1262
1803
|
const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
|
|
1263
|
-
const paramSite = (0,
|
|
1804
|
+
const paramSite = (0, boundary_codecs_1.getBoundaryParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
|
|
1264
1805
|
lines.push(` const ${pType} ${p.name} = ${paramSite ? `decode${paramSite.codecId}(args.value(${i}))` : variantToCppExpression(pType, `args.value(${i})`)};`);
|
|
1265
1806
|
}
|
|
1266
1807
|
const argNames = member.parameters.map((p) => p.name).join(", ");
|
|
@@ -1277,7 +1818,7 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1277
1818
|
if (member.kind !== "Input" || !member.payloadTypeText)
|
|
1278
1819
|
continue;
|
|
1279
1820
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1280
|
-
const payloadSite = (0,
|
|
1821
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1281
1822
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
1282
1823
|
lines.push(` const ${cppType} typedValue = ${payloadSite ? `decode${payloadSite.codecId}(value)` : variantToCppExpression(cppType, "value")};`);
|
|
1283
1824
|
lines.push(` set${pascalCase(member.name)}(typedValue);`);
|
|
@@ -1299,13 +1840,13 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1299
1840
|
}
|
|
1300
1841
|
if (member.kind === "Slot") {
|
|
1301
1842
|
const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
|
|
1302
|
-
const payloadSite = member.payloadTypeText ? (0,
|
|
1843
|
+
const payloadSite = member.payloadTypeText ? (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name) : undefined;
|
|
1303
1844
|
const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
|
|
1304
1845
|
lines.push(`${ret} ${widgetClassName}::slot_${member.name}(${args}) {`);
|
|
1305
1846
|
lines.push(` QVariantList invokeArgs;`);
|
|
1306
1847
|
for (const p of member.parameters) {
|
|
1307
1848
|
const pType = mapTsTypeToCpp(p.typeText);
|
|
1308
|
-
const paramSite = (0,
|
|
1849
|
+
const paramSite = (0, boundary_codecs_1.getBoundaryParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
|
|
1309
1850
|
lines.push(` invokeArgs.push_back(${paramSite ? `encode${paramSite.codecId}(${p.name})` : cppToVariantExpression(pType, p.name)});`);
|
|
1310
1851
|
}
|
|
1311
1852
|
lines.push(` QVariant result;`);
|
|
@@ -1330,7 +1871,7 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1330
1871
|
}
|
|
1331
1872
|
else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
|
|
1332
1873
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1333
|
-
const payloadSite = (0,
|
|
1874
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1334
1875
|
const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
|
|
1335
1876
|
lines.push(`${cppType} ${widgetClassName}::${member.name}() const {`);
|
|
1336
1877
|
lines.push(` return m_${member.name};`);
|
|
@@ -1529,7 +2070,8 @@ function renderNpmPackage(spec) {
|
|
|
1529
2070
|
anqst: {
|
|
1530
2071
|
widget: spec.widgetName,
|
|
1531
2072
|
services: spec.services.map((s) => s.name),
|
|
1532
|
-
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport
|
|
2073
|
+
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport,
|
|
2074
|
+
outputContractVersion: 2
|
|
1533
2075
|
}
|
|
1534
2076
|
}, null, 2);
|
|
1535
2077
|
}
|
|
@@ -1558,6 +2100,19 @@ function slotHandlerReturnType(tsRet) {
|
|
|
1558
2100
|
}
|
|
1559
2101
|
return `${tsRet} | Promise<${tsRet}> | Error`;
|
|
1560
2102
|
}
|
|
2103
|
+
/** Angular and vanilla: emit `readonly set` / `readonly onSlot` only when the spec provides members for that namespace. */
|
|
2104
|
+
function formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers) {
|
|
2105
|
+
const blocks = [];
|
|
2106
|
+
if (setMembers.length > 0) {
|
|
2107
|
+
blocks.push(` readonly set = {\n${setMembers.join("\n")}\n };`);
|
|
2108
|
+
}
|
|
2109
|
+
if (onSlotMembers.length > 0) {
|
|
2110
|
+
blocks.push(` readonly onSlot = {\n${onSlotMembers.join("\n")}\n };`);
|
|
2111
|
+
}
|
|
2112
|
+
if (blocks.length === 0)
|
|
2113
|
+
return "";
|
|
2114
|
+
return `\n${blocks.join("\n")}`;
|
|
2115
|
+
}
|
|
1561
2116
|
function renderTsService(spec, serviceName, codecCatalog) {
|
|
1562
2117
|
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
1563
2118
|
const fieldLines = [];
|
|
@@ -1567,11 +2122,11 @@ function renderTsService(spec, serviceName, codecCatalog) {
|
|
|
1567
2122
|
const constructorBodyLines = [];
|
|
1568
2123
|
for (const m of members) {
|
|
1569
2124
|
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
1570
|
-
const paramSites = m.parameters.map((p) => (0,
|
|
2125
|
+
const paramSites = m.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, serviceName, m.name, p.name));
|
|
1571
2126
|
const encodedValueArray = paramSites.length > 0
|
|
1572
2127
|
? `[${m.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
1573
2128
|
: "[]";
|
|
1574
|
-
const payloadSite = (0,
|
|
2129
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, serviceName, m.name);
|
|
1575
2130
|
if (m.kind === "Call") {
|
|
1576
2131
|
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
1577
2132
|
if (payloadSite) {
|
|
@@ -1649,30 +2204,18 @@ function renderTsService(spec, serviceName, codecCatalog) {
|
|
|
1649
2204
|
}
|
|
1650
2205
|
if (m.kind === "Output") {
|
|
1651
2206
|
constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => {`);
|
|
1652
|
-
constructorBodyLines.push(`
|
|
1653
|
-
constructorBodyLines.push(` this._${m.name}.set(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${tsType}`});`);
|
|
1654
|
-
constructorBodyLines.push(` } catch (error) {`);
|
|
1655
|
-
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
1656
|
-
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
1657
|
-
constructorBodyLines.push(` severity: "error",`);
|
|
1658
|
-
constructorBodyLines.push(` category: "bridge",`);
|
|
1659
|
-
constructorBodyLines.push(` recoverable: true,`);
|
|
1660
|
-
constructorBodyLines.push(` message: \`Failed to deserialize Output ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
1661
|
-
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
1662
|
-
constructorBodyLines.push(` member: "${m.name}",`);
|
|
1663
|
-
constructorBodyLines.push(` context: { interaction: "Output" }`);
|
|
1664
|
-
constructorBodyLines.push(` });`);
|
|
1665
|
-
constructorBodyLines.push(` }`);
|
|
2207
|
+
constructorBodyLines.push(` this._${m.name}.set(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${tsType}`});`);
|
|
1666
2208
|
constructorBodyLines.push(` });`);
|
|
1667
2209
|
}
|
|
1668
2210
|
}
|
|
1669
2211
|
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
1670
2212
|
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
2213
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
1671
2214
|
fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
1672
2215
|
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
|
|
1673
2216
|
constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
1674
2217
|
constructorBodyLines.push(` try {`);
|
|
1675
|
-
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `
|
|
2218
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
1676
2219
|
constructorBodyLines.push(` } catch (error) {`);
|
|
1677
2220
|
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
1678
2221
|
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
@@ -1689,11 +2232,12 @@ function renderTsService(spec, serviceName, codecCatalog) {
|
|
|
1689
2232
|
}
|
|
1690
2233
|
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
1691
2234
|
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
2235
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
1692
2236
|
fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
1693
2237
|
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
|
|
1694
2238
|
constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
1695
2239
|
constructorBodyLines.push(` try {`);
|
|
1696
|
-
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `
|
|
2240
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
1697
2241
|
constructorBodyLines.push(` } catch (error) {`);
|
|
1698
2242
|
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
1699
2243
|
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
@@ -1719,14 +2263,7 @@ function renderTsService(spec, serviceName, codecCatalog) {
|
|
|
1719
2263
|
export class ${serviceName} {
|
|
1720
2264
|
private readonly _bridge = inject(AnQstBridgeRuntime);
|
|
1721
2265
|
${fieldLines.join("\n")}
|
|
1722
|
-
${constructorLines.join("\n")}
|
|
1723
|
-
readonly set = {
|
|
1724
|
-
${setMembers.join("\n")}
|
|
1725
|
-
};
|
|
1726
|
-
readonly onSlot = {
|
|
1727
|
-
${onSlotMembers.join("\n")}
|
|
1728
|
-
};
|
|
1729
|
-
${methodLines.join("\n")}
|
|
2266
|
+
${constructorLines.join("\n")}${formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers)}${methodLines.length > 0 ? `\n${methodLines.join("\n")}` : ""}
|
|
1730
2267
|
}
|
|
1731
2268
|
`;
|
|
1732
2269
|
}
|
|
@@ -1769,34 +2306,40 @@ function renderTsServiceDts(spec, serviceName) {
|
|
|
1769
2306
|
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
1770
2307
|
}
|
|
1771
2308
|
}
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
readonly onSlot: ${onSlotInterfaceName}
|
|
2309
|
+
const interfaceBlocks = [];
|
|
2310
|
+
if (setMembers.length > 0) {
|
|
2311
|
+
interfaceBlocks.push(`export interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`);
|
|
2312
|
+
}
|
|
2313
|
+
if (onSlotMembers.length > 0) {
|
|
2314
|
+
interfaceBlocks.push(`export interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`);
|
|
2315
|
+
}
|
|
2316
|
+
const interfaceSection = interfaceBlocks.length > 0 ? `${interfaceBlocks.join("\n\n")}\n\n` : "";
|
|
2317
|
+
const namespaceLines = [];
|
|
2318
|
+
if (setMembers.length > 0) {
|
|
2319
|
+
namespaceLines.push(` readonly set: ${setInterfaceName};`);
|
|
2320
|
+
}
|
|
2321
|
+
if (onSlotMembers.length > 0) {
|
|
2322
|
+
namespaceLines.push(` readonly onSlot: ${onSlotInterfaceName};`);
|
|
2323
|
+
}
|
|
2324
|
+
const declareBodyLines = [...namespaceLines, ...classMembers];
|
|
2325
|
+
return `${interfaceSection}export declare class ${serviceName} {
|
|
2326
|
+
${declareBodyLines.join("\n")}
|
|
1786
2327
|
}`;
|
|
1787
2328
|
}
|
|
1788
|
-
function renderTsServices(spec) {
|
|
1789
|
-
const codecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
2329
|
+
function renderTsServices(spec, codecCatalog) {
|
|
1790
2330
|
const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name, codecCatalog)).join("\n");
|
|
1791
|
-
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/services.ts`).trim();
|
|
2331
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/services.ts`).trim();
|
|
1792
2332
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
1793
2333
|
const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
|
|
1794
2334
|
const typeImportsBlock = typeImports.length > 0 ? `${typeImports}\n\n` : "";
|
|
2335
|
+
const dragDropHelperBlock = renderTsDragDropPayloadHelpers(spec, codecCatalog).trim();
|
|
2336
|
+
const dragDropHelpers = dragDropHelperBlock.length > 0 ? `\n// Drag/drop payload helpers\n${dragDropHelperBlock}\n` : "";
|
|
1795
2337
|
return `import { Injectable, inject, signal } from "@angular/core";
|
|
1796
2338
|
${typeImportsBlock}
|
|
1797
2339
|
|
|
1798
|
-
//
|
|
1799
|
-
${(0,
|
|
2340
|
+
// Boundary codec plan helpers
|
|
2341
|
+
${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
|
|
2342
|
+
${dragDropHelpers}
|
|
1800
2343
|
|
|
1801
2344
|
type SlotHandler = (...args: unknown[]) => unknown;
|
|
1802
2345
|
type OutputHandler = (value: unknown) => void;
|
|
@@ -2603,13 +3146,13 @@ ${serviceClasses}
|
|
|
2603
3146
|
`;
|
|
2604
3147
|
}
|
|
2605
3148
|
function renderTsTypes(spec) {
|
|
2606
|
-
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types.ts`).trim();
|
|
3149
|
+
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types.ts`).trim();
|
|
2607
3150
|
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
2608
3151
|
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
2609
3152
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
2610
3153
|
}
|
|
2611
3154
|
function renderTypeServicesDts(spec) {
|
|
2612
|
-
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/services.d.ts`).trim();
|
|
3155
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types/services.d.ts`).trim();
|
|
2613
3156
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
2614
3157
|
const bridgeDiagnosticsDecl = `export type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
2615
3158
|
|
|
@@ -2646,7 +3189,7 @@ export declare class AnQstBridgeDiagnostics {
|
|
|
2646
3189
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
2647
3190
|
}
|
|
2648
3191
|
function renderTypeTypesDts(spec) {
|
|
2649
|
-
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/types.d.ts`).trim();
|
|
3192
|
+
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types/types.d.ts`).trim();
|
|
2650
3193
|
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
2651
3194
|
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
2652
3195
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
@@ -2675,74 +3218,1244 @@ function renderJsServices() {
|
|
|
2675
3218
|
function renderJsTypes() {
|
|
2676
3219
|
return renderJsModule();
|
|
2677
3220
|
}
|
|
2678
|
-
function
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
}, null, 2);
|
|
2697
|
-
}
|
|
2698
|
-
function nodeParamTuple(member) {
|
|
2699
|
-
if (member.parameters.length === 0)
|
|
2700
|
-
return "[]";
|
|
2701
|
-
return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
|
|
2702
|
-
}
|
|
2703
|
-
function nodeParamArgs(member) {
|
|
2704
|
-
return member.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
2705
|
-
}
|
|
2706
|
-
function nodeParamValues(member) {
|
|
2707
|
-
if (member.parameters.length === 0)
|
|
2708
|
-
return "[]";
|
|
2709
|
-
return `[${member.parameters.map((p) => p.name).join(", ")}]`;
|
|
2710
|
-
}
|
|
2711
|
-
function nodeCap(value) {
|
|
2712
|
-
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
2713
|
-
}
|
|
2714
|
-
function renderNodeExpressWsTypes(spec) {
|
|
2715
|
-
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
|
|
2716
|
-
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
2717
|
-
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
2718
|
-
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
2719
|
-
}
|
|
2720
|
-
function renderNodeExpressWsIndex(spec) {
|
|
2721
|
-
const codecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
2722
|
-
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
|
|
2723
|
-
const typeDecls = renderTypeDeclarations(spec, true);
|
|
2724
|
-
const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
|
|
2725
|
-
const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
|
|
2726
|
-
const handlerInterfaces = spec.services
|
|
2727
|
-
.map((service) => {
|
|
2728
|
-
const lines = [];
|
|
2729
|
-
for (const member of service.members) {
|
|
2730
|
-
const args = nodeParamArgs(member);
|
|
2731
|
-
const prefixedArgs = args.length > 0 ? `, ${args}` : "";
|
|
2732
|
-
if (member.kind === "Call" && member.payloadTypeText) {
|
|
2733
|
-
const ret = mapTypeTextToTs(member.payloadTypeText);
|
|
2734
|
-
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): ${ret} | Promise<${ret}>;`);
|
|
2735
|
-
}
|
|
2736
|
-
else if (member.kind === "Emitter") {
|
|
2737
|
-
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): void | Promise<void>;`);
|
|
3221
|
+
function renderVanillaServiceTs(spec, serviceName, codecCatalog) {
|
|
3222
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
3223
|
+
const fieldLines = [];
|
|
3224
|
+
const methodLines = [];
|
|
3225
|
+
const setMembers = [];
|
|
3226
|
+
const onSlotMembers = [];
|
|
3227
|
+
const constructorBodyLines = [];
|
|
3228
|
+
for (const m of members) {
|
|
3229
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
3230
|
+
const paramSites = m.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, serviceName, m.name, p.name));
|
|
3231
|
+
const encodedValueArray = paramSites.length > 0
|
|
3232
|
+
? `[${m.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
3233
|
+
: "[]";
|
|
3234
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, serviceName, m.name);
|
|
3235
|
+
if (m.kind === "Call") {
|
|
3236
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
3237
|
+
if (payloadSite) {
|
|
3238
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { const result = await this._bridge.call<unknown>("${serviceName}", "${m.name}", ${encodedValueArray}); return decode${payloadSite.codecId}(result); }`);
|
|
2738
3239
|
}
|
|
2739
|
-
else
|
|
2740
|
-
|
|
3240
|
+
else {
|
|
3241
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${encodedValueArray}); }`);
|
|
2741
3242
|
}
|
|
3243
|
+
continue;
|
|
2742
3244
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
3245
|
+
if (m.kind === "Emitter") {
|
|
3246
|
+
methodLines.push(` ${m.name}(${args}): void {`);
|
|
3247
|
+
methodLines.push(` let encodedArgs: unknown[];`);
|
|
3248
|
+
methodLines.push(` try {`);
|
|
3249
|
+
methodLines.push(` encodedArgs = ${encodedValueArray};`);
|
|
3250
|
+
methodLines.push(` } catch (error) {`);
|
|
3251
|
+
methodLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3252
|
+
methodLines.push(` code: "SerializationError",`);
|
|
3253
|
+
methodLines.push(` severity: "error",`);
|
|
3254
|
+
methodLines.push(` category: "bridge",`);
|
|
3255
|
+
methodLines.push(` recoverable: true,`);
|
|
3256
|
+
methodLines.push(` message: \`Failed to serialize Emitter ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3257
|
+
methodLines.push(` service: "${serviceName}",`);
|
|
3258
|
+
methodLines.push(` member: "${m.name}",`);
|
|
3259
|
+
methodLines.push(` context: { interaction: "Emitter" }`);
|
|
3260
|
+
methodLines.push(` });`);
|
|
3261
|
+
methodLines.push(` return;`);
|
|
3262
|
+
methodLines.push(` }`);
|
|
3263
|
+
methodLines.push(` this._bridge.emit("${serviceName}", "${m.name}", encodedArgs);`);
|
|
3264
|
+
methodLines.push(` }`);
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
if (m.kind === "Slot") {
|
|
3268
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
3269
|
+
const decodedArgs = m.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(wireArgs[${index}])` : `wireArgs[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ");
|
|
3270
|
+
onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
|
|
3271
|
+
onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", (...wireArgs: unknown[]) => {`);
|
|
3272
|
+
onSlotMembers.push(` const result = handler(${decodedArgs});`);
|
|
3273
|
+
if (payloadSite) {
|
|
3274
|
+
onSlotMembers.push(` if (result instanceof Promise) return result.then((value) => value instanceof Error ? value : encode${payloadSite.codecId}(value));`);
|
|
3275
|
+
onSlotMembers.push(` return result instanceof Error ? result : encode${payloadSite.codecId}(result);`);
|
|
3276
|
+
}
|
|
3277
|
+
else {
|
|
3278
|
+
onSlotMembers.push(" return result;");
|
|
3279
|
+
}
|
|
3280
|
+
onSlotMembers.push(" });");
|
|
3281
|
+
onSlotMembers.push(" },");
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
3285
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3286
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<${tsType} | undefined>(undefined);`);
|
|
3287
|
+
methodLines.push(` ${m.name}(): ${tsType} | undefined { return this._${m.name}.get(); }`);
|
|
3288
|
+
if (m.kind === "Input") {
|
|
3289
|
+
setMembers.push(` ${m.name}: (value: ${tsType}): void => {`);
|
|
3290
|
+
setMembers.push(` let encodedValue: unknown;`);
|
|
3291
|
+
setMembers.push(` try {`);
|
|
3292
|
+
setMembers.push(` encodedValue = ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"};`);
|
|
3293
|
+
setMembers.push(` } catch (error) {`);
|
|
3294
|
+
setMembers.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3295
|
+
setMembers.push(` code: "SerializationError",`);
|
|
3296
|
+
setMembers.push(` severity: "error",`);
|
|
3297
|
+
setMembers.push(` category: "bridge",`);
|
|
3298
|
+
setMembers.push(` recoverable: true,`);
|
|
3299
|
+
setMembers.push(` message: \`Failed to serialize Input ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3300
|
+
setMembers.push(` service: "${serviceName}",`);
|
|
3301
|
+
setMembers.push(` member: "${m.name}",`);
|
|
3302
|
+
setMembers.push(` context: { interaction: "Input" }`);
|
|
3303
|
+
setMembers.push(` });`);
|
|
3304
|
+
setMembers.push(` return;`);
|
|
3305
|
+
setMembers.push(` }`);
|
|
3306
|
+
setMembers.push(` this._${m.name}.set(value);`);
|
|
3307
|
+
setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", encodedValue);`);
|
|
3308
|
+
setMembers.push(" },");
|
|
3309
|
+
}
|
|
3310
|
+
if (m.kind === "Output") {
|
|
3311
|
+
constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => {`);
|
|
3312
|
+
constructorBodyLines.push(` this._${m.name}.set(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${tsType}`});`);
|
|
3313
|
+
constructorBodyLines.push(` });`);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
3317
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3318
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
3319
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
3320
|
+
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}.get(); }`);
|
|
3321
|
+
constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
3322
|
+
constructorBodyLines.push(` try {`);
|
|
3323
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
3324
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
3325
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3326
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
3327
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
3328
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
3329
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
3330
|
+
constructorBodyLines.push(` message: \`Failed to deserialize DropTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3331
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
3332
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
3333
|
+
constructorBodyLines.push(` context: { interaction: "DropTarget" }`);
|
|
3334
|
+
constructorBodyLines.push(` });`);
|
|
3335
|
+
constructorBodyLines.push(` }`);
|
|
3336
|
+
constructorBodyLines.push(` });`);
|
|
3337
|
+
}
|
|
3338
|
+
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
3339
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3340
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
3341
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
3342
|
+
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}.get(); }`);
|
|
3343
|
+
constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
3344
|
+
constructorBodyLines.push(` try {`);
|
|
3345
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
3346
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
3347
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3348
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
3349
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
3350
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
3351
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
3352
|
+
constructorBodyLines.push(` message: \`Failed to deserialize HoverTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3353
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
3354
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
3355
|
+
constructorBodyLines.push(` context: { interaction: "HoverTarget" }`);
|
|
3356
|
+
constructorBodyLines.push(` });`);
|
|
3357
|
+
constructorBodyLines.push(` }`);
|
|
3358
|
+
constructorBodyLines.push(` });`);
|
|
3359
|
+
constructorBodyLines.push(` this._bridge.onHoverLeft("${serviceName}", "${m.name}", () => this._${m.name}.set(null));`);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
const constructorLines = [
|
|
3363
|
+
" constructor(private readonly _bridge: AnQstBridgeRuntime) {",
|
|
3364
|
+
...constructorBodyLines,
|
|
3365
|
+
" }",
|
|
3366
|
+
];
|
|
3367
|
+
return `class ${serviceName} {
|
|
3368
|
+
${fieldLines.join("\n")}
|
|
3369
|
+
${constructorLines.join("\n")}${formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers)}${methodLines.length > 0 ? `\n${methodLines.join("\n")}` : ""}
|
|
3370
|
+
}
|
|
3371
|
+
`;
|
|
3372
|
+
}
|
|
3373
|
+
function renderVanillaBrowserTs(spec, codecCatalog) {
|
|
3374
|
+
const localTypeDecls = renderTypeDeclarations(spec).trim();
|
|
3375
|
+
const localTypesBlock = localTypeDecls.length > 0 ? `${localTypeDecls}\n\n` : "";
|
|
3376
|
+
const dragDropHelperBlock = renderTsDragDropPayloadHelpers(spec, codecCatalog).trim();
|
|
3377
|
+
const dragDropHelpers = dragDropHelperBlock.length > 0 ? `\n// Drag/drop payload helpers\n${dragDropHelperBlock}\n` : "";
|
|
3378
|
+
const serviceClasses = spec.services.map((s) => renderVanillaServiceTs(spec, s.name, codecCatalog)).join("\n");
|
|
3379
|
+
const frontendServices = spec.services.length > 0
|
|
3380
|
+
? spec.services.map((s) => ` ${s.name}: ${s.name};`).join("\n")
|
|
3381
|
+
: "";
|
|
3382
|
+
const frontendServiceFactories = spec.services.length > 0
|
|
3383
|
+
? spec.services.map((s) => ` ${s.name}: new ${s.name}(bridge)`).join(",\n")
|
|
3384
|
+
: "";
|
|
3385
|
+
return `${localTypesBlock}// Boundary codec plan helpers
|
|
3386
|
+
${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
|
|
3387
|
+
${dragDropHelpers}
|
|
3388
|
+
|
|
3389
|
+
type SlotHandler = (...args: unknown[]) => unknown;
|
|
3390
|
+
type OutputHandler = (value: unknown) => void;
|
|
3391
|
+
type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
|
|
3392
|
+
type OutputListener = (service: string, member: string, value: unknown) => void;
|
|
3393
|
+
type DropListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
3394
|
+
type HoverListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
3395
|
+
type HoverLeftListener = (service: string, member: string) => void;
|
|
3396
|
+
type HostDiagnosticListener = (payload: unknown) => void;
|
|
3397
|
+
type DisconnectListener = () => void;
|
|
3398
|
+
|
|
3399
|
+
type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
3400
|
+
type AnQstBridgeSource = "frontend" | "host";
|
|
3401
|
+
type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
3402
|
+
type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
3403
|
+
|
|
3404
|
+
interface AnQstBridgeDiagnostic {
|
|
3405
|
+
code: string;
|
|
3406
|
+
severity: AnQstBridgeSeverity;
|
|
3407
|
+
category: string;
|
|
3408
|
+
recoverable: boolean;
|
|
3409
|
+
message: string;
|
|
3410
|
+
timestamp: string;
|
|
3411
|
+
source: AnQstBridgeSource;
|
|
3412
|
+
transport?: AnQstBridgeTransport;
|
|
3413
|
+
service?: string;
|
|
3414
|
+
member?: string;
|
|
3415
|
+
requestId?: string;
|
|
3416
|
+
context?: Record<string, unknown>;
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
interface HostBridgeApi {
|
|
3420
|
+
anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
|
|
3421
|
+
anQstBridge_emit(service: string, member: string, args: unknown[]): void;
|
|
3422
|
+
anQstBridge_setInput(service: string, member: string, value: unknown): void;
|
|
3423
|
+
anQstBridge_registerSlot(service: string, member: string): void;
|
|
3424
|
+
anQstBridge_resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
3425
|
+
anQstBridge_outputUpdated: { connect: (cb: (service: string, member: string, value: unknown) => void) => void };
|
|
3426
|
+
anQstBridge_slotInvocationRequested: {
|
|
3427
|
+
connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
|
|
3428
|
+
};
|
|
3429
|
+
anQstBridge_hostDiagnostic?: { connect: (cb: (payload: unknown) => void) => void };
|
|
3430
|
+
anQstBridge_dropReceived: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
3431
|
+
anQstBridge_hoverUpdated: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
3432
|
+
anQstBridge_hoverLeft: { connect: (cb: (service: string, member: string) => void) => void };
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
interface QWebChannelCtor {
|
|
3436
|
+
new (
|
|
3437
|
+
transport: unknown,
|
|
3438
|
+
initCallback: (channel: { objects: Record<string, HostBridgeApi | undefined> }) => void
|
|
3439
|
+
): unknown;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
interface BridgeAdapter {
|
|
3443
|
+
readonly transport: AnQstBridgeTransport;
|
|
3444
|
+
call<T>(service: string, member: string, args: unknown[]): Promise<T>;
|
|
3445
|
+
emit(service: string, member: string, args: unknown[]): void;
|
|
3446
|
+
setInput(service: string, member: string, value: unknown): void;
|
|
3447
|
+
registerSlot(service: string, member: string): void;
|
|
3448
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
3449
|
+
onOutput(handler: OutputListener): void;
|
|
3450
|
+
onSlotInvocation(handler: SlotInvocationListener): void;
|
|
3451
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void;
|
|
3452
|
+
onDisconnected(handler: DisconnectListener): void;
|
|
3453
|
+
onDrop(handler: DropListener): void;
|
|
3454
|
+
onHover(handler: HoverListener): void;
|
|
3455
|
+
onHoverLeft(handler: HoverLeftListener): void;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
interface ValueCell<T> {
|
|
3459
|
+
get(): T;
|
|
3460
|
+
set(value: T): void;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function createValueCell<T>(initial: T): ValueCell<T> {
|
|
3464
|
+
let current = initial;
|
|
3465
|
+
return {
|
|
3466
|
+
get(): T {
|
|
3467
|
+
return current;
|
|
3468
|
+
},
|
|
3469
|
+
set(value: T): void {
|
|
3470
|
+
current = value;
|
|
3471
|
+
}
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function errorMessage(error: unknown): string {
|
|
3476
|
+
if (error instanceof Error && typeof error.message === "string" && error.message.length > 0) {
|
|
3477
|
+
return error.message;
|
|
3478
|
+
}
|
|
3479
|
+
return String(error);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
function normalizeSeverity(value: unknown): AnQstBridgeSeverity {
|
|
3483
|
+
if (value === "info" || value === "warn" || value === "error" || value === "fatal") {
|
|
3484
|
+
return value;
|
|
3485
|
+
}
|
|
3486
|
+
return "error";
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
3490
|
+
if (value === null || typeof value !== "object") {
|
|
3491
|
+
return undefined;
|
|
3492
|
+
}
|
|
3493
|
+
return value as Record<string, unknown>;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
function readString(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
3497
|
+
const value = record?.[key];
|
|
3498
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
function readBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
|
|
3502
|
+
const value = record?.[key];
|
|
3503
|
+
return typeof value === "boolean" ? value : undefined;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
function readContext(record: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
3507
|
+
const context = asRecord(record?.["context"]);
|
|
3508
|
+
return context === undefined ? undefined : context;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function normalizeHostDiagnostic(payload: unknown, transport: AnQstBridgeTransport): Omit<AnQstBridgeDiagnostic, "timestamp"> {
|
|
3512
|
+
const row = asRecord(payload);
|
|
3513
|
+
if (row === undefined) {
|
|
3514
|
+
return {
|
|
3515
|
+
code: "HostDiagnosticMalformed",
|
|
3516
|
+
severity: "error",
|
|
3517
|
+
category: "bridge",
|
|
3518
|
+
recoverable: true,
|
|
3519
|
+
message: "Host emitted a malformed diagnostic payload.",
|
|
3520
|
+
source: "host",
|
|
3521
|
+
transport
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
const context = readContext(row);
|
|
3526
|
+
return {
|
|
3527
|
+
code: readString(row, "code") ?? "HostDiagnostic",
|
|
3528
|
+
severity: normalizeSeverity(row["severity"]),
|
|
3529
|
+
category: readString(row, "category") ?? "bridge",
|
|
3530
|
+
recoverable: readBoolean(row, "recoverable") ?? true,
|
|
3531
|
+
message: readString(row, "message") ?? "Host emitted a diagnostic payload.",
|
|
3532
|
+
source: "host",
|
|
3533
|
+
transport,
|
|
3534
|
+
service: readString(row, "service") ?? readString(context, "service"),
|
|
3535
|
+
member: readString(row, "member") ?? readString(context, "member"),
|
|
3536
|
+
requestId: readString(row, "requestId") ?? readString(context, "requestId"),
|
|
3537
|
+
context
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
function isBridgeCallError(value: unknown): value is {
|
|
3542
|
+
code: unknown;
|
|
3543
|
+
message: unknown;
|
|
3544
|
+
service: unknown;
|
|
3545
|
+
member: unknown;
|
|
3546
|
+
requestId: unknown;
|
|
3547
|
+
} {
|
|
3548
|
+
if (value === null || typeof value !== "object") return false;
|
|
3549
|
+
const row = value as Record<string, unknown>;
|
|
3550
|
+
return (
|
|
3551
|
+
Object.prototype.hasOwnProperty.call(row, "code")
|
|
3552
|
+
&& Object.prototype.hasOwnProperty.call(row, "message")
|
|
3553
|
+
&& Object.prototype.hasOwnProperty.call(row, "service")
|
|
3554
|
+
&& Object.prototype.hasOwnProperty.call(row, "member")
|
|
3555
|
+
&& Object.prototype.hasOwnProperty.call(row, "requestId")
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
class QtWebChannelAdapter implements BridgeAdapter {
|
|
3560
|
+
readonly transport = "qt-webchannel" as const;
|
|
3561
|
+
|
|
3562
|
+
private constructor(private readonly host: HostBridgeApi) {}
|
|
3563
|
+
|
|
3564
|
+
static async create(): Promise<QtWebChannelAdapter> {
|
|
3565
|
+
const anyWindow = window as unknown as {
|
|
3566
|
+
qt?: { webChannelTransport?: unknown };
|
|
3567
|
+
QWebChannel?: QWebChannelCtor;
|
|
3568
|
+
};
|
|
3569
|
+
if (typeof anyWindow.QWebChannel !== "function" || anyWindow.qt?.webChannelTransport === undefined) {
|
|
3570
|
+
throw new Error("Qt WebChannel transport is unavailable.");
|
|
3571
|
+
}
|
|
3572
|
+
return await new Promise<QtWebChannelAdapter>((resolve, reject) => {
|
|
3573
|
+
try {
|
|
3574
|
+
const QWebChannel = anyWindow.QWebChannel as QWebChannelCtor;
|
|
3575
|
+
new QWebChannel(anyWindow.qt!.webChannelTransport, (channel) => {
|
|
3576
|
+
try {
|
|
3577
|
+
const host = channel.objects["${spec.widgetName}Bridge"];
|
|
3578
|
+
if (host === undefined) {
|
|
3579
|
+
reject(new Error("${spec.widgetName}Bridge bridge object is unavailable."));
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
resolve(new QtWebChannelAdapter(host));
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3589
|
+
}
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3594
|
+
return new Promise<T>((resolve, reject) => {
|
|
3595
|
+
this.host.anQstBridge_call(service, member, args, (result) => {
|
|
3596
|
+
if (isBridgeCallError(result)) {
|
|
3597
|
+
reject(result);
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
resolve(result as T);
|
|
3601
|
+
});
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3606
|
+
this.host.anQstBridge_emit(service, member, args);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3610
|
+
this.host.anQstBridge_setInput(service, member, value);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
registerSlot(service: string, member: string): void {
|
|
3614
|
+
this.host.anQstBridge_registerSlot(service, member);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
3618
|
+
this.host.anQstBridge_resolveSlot(requestId, ok, payload, error);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
onOutput(handler: OutputListener): void {
|
|
3622
|
+
this.host.anQstBridge_outputUpdated.connect(handler);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
3626
|
+
this.host.anQstBridge_slotInvocationRequested.connect(handler);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
3630
|
+
this.host.anQstBridge_hostDiagnostic?.connect(handler);
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
onDisconnected(_handler: DisconnectListener): void {
|
|
3634
|
+
// QWebChannel does not expose a deterministic disconnect event here.
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
onDrop(handler: DropListener): void {
|
|
3638
|
+
this.host.anQstBridge_dropReceived.connect(handler);
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
onHover(handler: HoverListener): void {
|
|
3642
|
+
this.host.anQstBridge_hoverUpdated.connect(handler);
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
onHoverLeft(handler: HoverLeftListener): void {
|
|
3646
|
+
this.host.anQstBridge_hoverLeft.connect(handler);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
3651
|
+
readonly transport = "dev-websocket" as const;
|
|
3652
|
+
private readonly pending = new Map<string, {
|
|
3653
|
+
service: string;
|
|
3654
|
+
member: string;
|
|
3655
|
+
requestId: string;
|
|
3656
|
+
resolve: (result: unknown) => void;
|
|
3657
|
+
reject: (error: unknown) => void;
|
|
3658
|
+
}>();
|
|
3659
|
+
private readonly outputListeners: OutputListener[] = [];
|
|
3660
|
+
private readonly slotListeners: SlotInvocationListener[] = [];
|
|
3661
|
+
private readonly hostDiagnosticListeners: HostDiagnosticListener[] = [];
|
|
3662
|
+
private readonly disconnectListeners: DisconnectListener[] = [];
|
|
3663
|
+
private readonly dropListeners: DropListener[] = [];
|
|
3664
|
+
private readonly hoverListeners: HoverListener[] = [];
|
|
3665
|
+
private readonly hoverLeftListeners: HoverLeftListener[] = [];
|
|
3666
|
+
private requestCounter = 0;
|
|
3667
|
+
|
|
3668
|
+
private constructor(private readonly socket: WebSocket) {
|
|
3669
|
+
this.socket.addEventListener("message", (event) => {
|
|
3670
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
3671
|
+
const message = JSON.parse(raw) as Record<string, unknown>;
|
|
3672
|
+
const type = String(message["type"] ?? "");
|
|
3673
|
+
if (type === "callResult") {
|
|
3674
|
+
const requestId = String(message["requestId"] ?? "");
|
|
3675
|
+
const pending = this.pending.get(requestId);
|
|
3676
|
+
if (pending) {
|
|
3677
|
+
this.pending.delete(requestId);
|
|
3678
|
+
const result = message["result"];
|
|
3679
|
+
if (isBridgeCallError(result)) {
|
|
3680
|
+
pending.reject(result);
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
pending.resolve(result);
|
|
3684
|
+
}
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
if (type === "outputUpdated") {
|
|
3688
|
+
const service = String(message["service"] ?? "");
|
|
3689
|
+
const member = String(message["member"] ?? "");
|
|
3690
|
+
for (const listener of this.outputListeners) {
|
|
3691
|
+
listener(service, member, message["value"]);
|
|
3692
|
+
}
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
if (type === "slotInvocationRequested") {
|
|
3696
|
+
const requestId = String(message["requestId"] ?? "");
|
|
3697
|
+
const service = String(message["service"] ?? "");
|
|
3698
|
+
const member = String(message["member"] ?? "");
|
|
3699
|
+
const args = Array.isArray(message["args"]) ? (message["args"] as unknown[]) : [];
|
|
3700
|
+
for (const listener of this.slotListeners) {
|
|
3701
|
+
listener(requestId, service, member, args);
|
|
3702
|
+
}
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (type === "dropReceived") {
|
|
3706
|
+
const service = String(message["service"] ?? "");
|
|
3707
|
+
const member = String(message["member"] ?? "");
|
|
3708
|
+
const x = Number(message["x"] ?? 0);
|
|
3709
|
+
const y = Number(message["y"] ?? 0);
|
|
3710
|
+
for (const listener of this.dropListeners) {
|
|
3711
|
+
listener(service, member, message["payload"], x, y);
|
|
3712
|
+
}
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
if (type === "hoverUpdated") {
|
|
3716
|
+
const service = String(message["service"] ?? "");
|
|
3717
|
+
const member = String(message["member"] ?? "");
|
|
3718
|
+
const x = Number(message["x"] ?? 0);
|
|
3719
|
+
const y = Number(message["y"] ?? 0);
|
|
3720
|
+
for (const listener of this.hoverListeners) {
|
|
3721
|
+
listener(service, member, message["payload"], x, y);
|
|
3722
|
+
}
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
if (type === "hoverLeft") {
|
|
3726
|
+
const service = String(message["service"] ?? "");
|
|
3727
|
+
const member = String(message["member"] ?? "");
|
|
3728
|
+
for (const listener of this.hoverLeftListeners) {
|
|
3729
|
+
listener(service, member);
|
|
3730
|
+
}
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
if (type === "hostError") {
|
|
3734
|
+
for (const listener of this.hostDiagnosticListeners) {
|
|
3735
|
+
listener(message["payload"]);
|
|
3736
|
+
}
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
if (type === "widgetReattached") {
|
|
3740
|
+
document.body.textContent = "Widget Reattached";
|
|
3741
|
+
this.socket.close();
|
|
3742
|
+
}
|
|
3743
|
+
});
|
|
3744
|
+
this.socket.addEventListener("close", () => {
|
|
3745
|
+
for (const pending of this.pending.values()) {
|
|
3746
|
+
pending.reject({
|
|
3747
|
+
code: "BridgeDisconnectedError",
|
|
3748
|
+
message: "Bridge disconnected before call completion.",
|
|
3749
|
+
service: pending.service,
|
|
3750
|
+
member: pending.member,
|
|
3751
|
+
requestId: pending.requestId
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
this.pending.clear();
|
|
3755
|
+
for (const listener of this.disconnectListeners) {
|
|
3756
|
+
listener();
|
|
3757
|
+
}
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
static async create(): Promise<WebSocketBridgeAdapter> {
|
|
3762
|
+
const configResponse = await fetch("/anqst-dev-config.json", { cache: "no-store" });
|
|
3763
|
+
if (!configResponse.ok) {
|
|
3764
|
+
throw new Error("AnQst host bootstrap missing: unable to read /anqst-dev-config.json");
|
|
3765
|
+
}
|
|
3766
|
+
const config = (await configResponse.json()) as { wsUrl?: string; wsPath?: string };
|
|
3767
|
+
let wsUrl = config.wsUrl;
|
|
3768
|
+
if (!wsUrl && config.wsPath) {
|
|
3769
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
3770
|
+
wsUrl = protocol + "//" + window.location.host + config.wsPath;
|
|
3771
|
+
}
|
|
3772
|
+
if (!wsUrl) {
|
|
3773
|
+
throw new Error("AnQst host bootstrap missing: wsUrl/wsPath is unavailable.");
|
|
3774
|
+
}
|
|
3775
|
+
if (wsUrl.startsWith("http://")) {
|
|
3776
|
+
wsUrl = "ws://" + wsUrl.slice("http://".length);
|
|
3777
|
+
} else if (wsUrl.startsWith("https://")) {
|
|
3778
|
+
wsUrl = "wss://" + wsUrl.slice("https://".length);
|
|
3779
|
+
}
|
|
3780
|
+
return await new Promise<WebSocketBridgeAdapter>((resolve, reject) => {
|
|
3781
|
+
const socket = new WebSocket(wsUrl!);
|
|
3782
|
+
socket.addEventListener("open", () => resolve(new WebSocketBridgeAdapter(socket)));
|
|
3783
|
+
socket.addEventListener("error", () => reject(new Error("Failed to connect to AnQst WebSocket bridge.")));
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3788
|
+
const requestId = \`req-\${++this.requestCounter}\`;
|
|
3789
|
+
const payload = { type: "call", requestId, service, member, args };
|
|
3790
|
+
return await new Promise<T>((resolve, reject) => {
|
|
3791
|
+
this.pending.set(requestId, {
|
|
3792
|
+
service,
|
|
3793
|
+
member,
|
|
3794
|
+
requestId,
|
|
3795
|
+
resolve: (value) => resolve(value as T),
|
|
3796
|
+
reject
|
|
3797
|
+
});
|
|
3798
|
+
this.socket.send(JSON.stringify(payload));
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3803
|
+
this.socket.send(JSON.stringify({ type: "emit", service, member, args }));
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3807
|
+
this.socket.send(JSON.stringify({ type: "setInput", service, member, value }));
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
registerSlot(service: string, member: string): void {
|
|
3811
|
+
this.socket.send(JSON.stringify({ type: "registerSlot", service, member }));
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
3815
|
+
this.socket.send(JSON.stringify({ type: "resolveSlot", requestId, ok, payload, error }));
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
onOutput(handler: OutputListener): void {
|
|
3819
|
+
this.outputListeners.push(handler);
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
3823
|
+
this.slotListeners.push(handler);
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
3827
|
+
this.hostDiagnosticListeners.push(handler);
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
onDisconnected(handler: DisconnectListener): void {
|
|
3831
|
+
this.disconnectListeners.push(handler);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
onDrop(handler: DropListener): void {
|
|
3835
|
+
this.dropListeners.push(handler);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
onHover(handler: HoverListener): void {
|
|
3839
|
+
this.hoverListeners.push(handler);
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
onHoverLeft(handler: HoverLeftListener): void {
|
|
3843
|
+
this.hoverLeftListeners.push(handler);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
class AnQstBridgeRuntime {
|
|
3848
|
+
private static readonly maxDiagnostics = 50;
|
|
3849
|
+
private adapter: BridgeAdapter | null = null;
|
|
3850
|
+
private readonly slotHandlers = new Map<string, SlotHandler>();
|
|
3851
|
+
private readonly outputHandlers = new Map<string, OutputHandler[]>();
|
|
3852
|
+
private readonly dropHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
3853
|
+
private readonly hoverHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
3854
|
+
private readonly hoverLeftHandlers = new Map<string, (() => void)[]>();
|
|
3855
|
+
private readonly diagnosticListeners = new Set<(diagnostic: AnQstBridgeDiagnostic) => void>();
|
|
3856
|
+
private readonly _diagnostics = createValueCell<readonly AnQstBridgeDiagnostic[]>([]);
|
|
3857
|
+
private readonly _state = createValueCell<AnQstBridgeState>("starting");
|
|
3858
|
+
private readonly startup = this.init().catch((error) => {
|
|
3859
|
+
this._state.set("failed");
|
|
3860
|
+
this.reportFrontendDiagnostic({
|
|
3861
|
+
code: "BridgeBootstrapError",
|
|
3862
|
+
severity: "fatal",
|
|
3863
|
+
category: "bridge",
|
|
3864
|
+
recoverable: false,
|
|
3865
|
+
message: \`Failed to initialize bridge: \${errorMessage(error)}\`
|
|
3866
|
+
});
|
|
3867
|
+
throw error;
|
|
3868
|
+
});
|
|
3869
|
+
|
|
3870
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
3871
|
+
return this._diagnostics.get();
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
state(): AnQstBridgeState {
|
|
3875
|
+
return this._state.get();
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
subscribeDiagnostics(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
3879
|
+
this.diagnosticListeners.add(listener);
|
|
3880
|
+
return () => this.diagnosticListeners.delete(listener);
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
async ready(): Promise<void> {
|
|
3884
|
+
return this.startup;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
reportFrontendDiagnostic(diagnostic: Omit<AnQstBridgeDiagnostic, "timestamp" | "source">): void {
|
|
3888
|
+
this.pushDiagnostic({
|
|
3889
|
+
...diagnostic,
|
|
3890
|
+
source: "frontend",
|
|
3891
|
+
transport: diagnostic.transport ?? this.adapter?.transport,
|
|
3892
|
+
timestamp: new Date().toISOString()
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3897
|
+
const adapter = await this.requireAdapter();
|
|
3898
|
+
return adapter.call<T>(service, member, args);
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3902
|
+
this.publishNonCall("Emitter", service, member, (adapter) => adapter.emit(service, member, args));
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3906
|
+
this.publishNonCall("Input", service, member, (adapter) => adapter.setInput(service, member, value));
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
registerSlot(service: string, member: string, handler: SlotHandler): void {
|
|
3910
|
+
const key = this.key(service, member);
|
|
3911
|
+
this.slotHandlers.set(key, handler);
|
|
3912
|
+
if (this.adapter !== null) {
|
|
3913
|
+
try {
|
|
3914
|
+
this.adapter.registerSlot(service, member);
|
|
3915
|
+
} catch (error) {
|
|
3916
|
+
this.reportFrontendDiagnostic({
|
|
3917
|
+
code: "BridgePublishError",
|
|
3918
|
+
severity: "error",
|
|
3919
|
+
category: "bridge",
|
|
3920
|
+
recoverable: true,
|
|
3921
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
3922
|
+
service,
|
|
3923
|
+
member,
|
|
3924
|
+
context: { interaction: "Slot" }
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
this.ready()
|
|
3930
|
+
.then(() => {
|
|
3931
|
+
try {
|
|
3932
|
+
this.requireAdapterSync().registerSlot(service, member);
|
|
3933
|
+
} catch (error) {
|
|
3934
|
+
this.reportFrontendDiagnostic({
|
|
3935
|
+
code: "BridgePublishError",
|
|
3936
|
+
severity: "error",
|
|
3937
|
+
category: "bridge",
|
|
3938
|
+
recoverable: true,
|
|
3939
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
3940
|
+
service,
|
|
3941
|
+
member,
|
|
3942
|
+
context: { interaction: "Slot" }
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
})
|
|
3946
|
+
.catch((error) => {
|
|
3947
|
+
this.reportFrontendDiagnostic({
|
|
3948
|
+
code: "BridgePublishError",
|
|
3949
|
+
severity: "error",
|
|
3950
|
+
category: "bridge",
|
|
3951
|
+
recoverable: true,
|
|
3952
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
3953
|
+
service,
|
|
3954
|
+
member,
|
|
3955
|
+
context: { interaction: "Slot" }
|
|
3956
|
+
});
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
onOutput(service: string, member: string, handler: OutputHandler): void {
|
|
3961
|
+
const key = this.key(service, member);
|
|
3962
|
+
const existing = this.outputHandlers.get(key) ?? [];
|
|
3963
|
+
existing.push(handler);
|
|
3964
|
+
this.outputHandlers.set(key, existing);
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
onDrop(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
|
|
3968
|
+
const key = this.key(service, member);
|
|
3969
|
+
const existing = this.dropHandlers.get(key) ?? [];
|
|
3970
|
+
existing.push(handler);
|
|
3971
|
+
this.dropHandlers.set(key, existing);
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
onHover(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
|
|
3975
|
+
const key = this.key(service, member);
|
|
3976
|
+
const existing = this.hoverHandlers.get(key) ?? [];
|
|
3977
|
+
existing.push(handler);
|
|
3978
|
+
this.hoverHandlers.set(key, existing);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
onHoverLeft(service: string, member: string, handler: () => void): void {
|
|
3982
|
+
const key = this.key(service, member);
|
|
3983
|
+
const existing = this.hoverLeftHandlers.get(key) ?? [];
|
|
3984
|
+
existing.push(handler);
|
|
3985
|
+
this.hoverLeftHandlers.set(key, existing);
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
private requireAdapterSync(): BridgeAdapter {
|
|
3989
|
+
if (this.adapter === null) {
|
|
3990
|
+
throw new Error("AnQst bridge is not ready.");
|
|
3991
|
+
}
|
|
3992
|
+
return this.adapter;
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
private async requireAdapter(): Promise<BridgeAdapter> {
|
|
3996
|
+
await this.startup;
|
|
3997
|
+
return this.requireAdapterSync();
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
private pushDiagnostic(diagnostic: AnQstBridgeDiagnostic): void {
|
|
4001
|
+
const previous = this._diagnostics.get();
|
|
4002
|
+
const trimmed = previous.length >= AnQstBridgeRuntime.maxDiagnostics
|
|
4003
|
+
? previous.slice(previous.length - (AnQstBridgeRuntime.maxDiagnostics - 1))
|
|
4004
|
+
: previous;
|
|
4005
|
+
const next = [...trimmed, diagnostic];
|
|
4006
|
+
this._diagnostics.set(next);
|
|
4007
|
+
for (const listener of this.diagnosticListeners) {
|
|
4008
|
+
listener(diagnostic);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
private publishNonCall(
|
|
4013
|
+
interaction: "Emitter" | "Input",
|
|
4014
|
+
service: string,
|
|
4015
|
+
member: string,
|
|
4016
|
+
publish: (adapter: BridgeAdapter) => void
|
|
4017
|
+
): void {
|
|
4018
|
+
if (this.adapter !== null) {
|
|
4019
|
+
try {
|
|
4020
|
+
publish(this.adapter);
|
|
4021
|
+
} catch (error) {
|
|
4022
|
+
this.reportFrontendDiagnostic({
|
|
4023
|
+
code: "BridgePublishError",
|
|
4024
|
+
severity: "error",
|
|
4025
|
+
category: "bridge",
|
|
4026
|
+
recoverable: true,
|
|
4027
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4028
|
+
service,
|
|
4029
|
+
member,
|
|
4030
|
+
context: { interaction }
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
this.ready()
|
|
4037
|
+
.then(() => {
|
|
4038
|
+
try {
|
|
4039
|
+
publish(this.requireAdapterSync());
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
this.reportFrontendDiagnostic({
|
|
4042
|
+
code: "BridgePublishError",
|
|
4043
|
+
severity: "error",
|
|
4044
|
+
category: "bridge",
|
|
4045
|
+
recoverable: true,
|
|
4046
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4047
|
+
service,
|
|
4048
|
+
member,
|
|
4049
|
+
context: { interaction }
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
})
|
|
4053
|
+
.catch((error) => {
|
|
4054
|
+
this.reportFrontendDiagnostic({
|
|
4055
|
+
code: "BridgePublishError",
|
|
4056
|
+
severity: "error",
|
|
4057
|
+
category: "bridge",
|
|
4058
|
+
recoverable: true,
|
|
4059
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4060
|
+
service,
|
|
4061
|
+
member,
|
|
4062
|
+
context: { interaction }
|
|
4063
|
+
});
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
private async init(): Promise<void> {
|
|
4068
|
+
const anyWindow = window as unknown as { qt?: { webChannelTransport?: unknown }; QWebChannel?: QWebChannelCtor };
|
|
4069
|
+
if (typeof anyWindow.QWebChannel === "function" && anyWindow.qt?.webChannelTransport !== undefined) {
|
|
4070
|
+
this.adapter = await QtWebChannelAdapter.create();
|
|
4071
|
+
} else {
|
|
4072
|
+
this.adapter = await WebSocketBridgeAdapter.create();
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
const adapter = this.adapter;
|
|
4076
|
+
adapter.onHostDiagnostic((payload) => {
|
|
4077
|
+
this.pushDiagnostic({
|
|
4078
|
+
...normalizeHostDiagnostic(payload, adapter.transport),
|
|
4079
|
+
timestamp: new Date().toISOString()
|
|
4080
|
+
});
|
|
4081
|
+
});
|
|
4082
|
+
adapter.onDisconnected(() => {
|
|
4083
|
+
this._state.set("disconnected");
|
|
4084
|
+
this.reportFrontendDiagnostic({
|
|
4085
|
+
code: "BridgeDisconnectedError",
|
|
4086
|
+
severity: "error",
|
|
4087
|
+
category: "bridge",
|
|
4088
|
+
recoverable: true,
|
|
4089
|
+
message: "Bridge disconnected.",
|
|
4090
|
+
transport: adapter.transport
|
|
4091
|
+
});
|
|
4092
|
+
});
|
|
4093
|
+
|
|
4094
|
+
adapter.onOutput((service, member, value) => {
|
|
4095
|
+
const key = this.key(service, member);
|
|
4096
|
+
for (const outputHandler of this.outputHandlers.get(key) ?? []) {
|
|
4097
|
+
outputHandler(value);
|
|
4098
|
+
}
|
|
4099
|
+
});
|
|
4100
|
+
adapter.onSlotInvocation(async (requestId, service, member, args) => {
|
|
4101
|
+
const key = this.key(service, member);
|
|
4102
|
+
const handler = this.slotHandlers.get(key);
|
|
4103
|
+
if (handler === undefined) {
|
|
4104
|
+
this.reportFrontendDiagnostic({
|
|
4105
|
+
code: "HandlerNotRegisteredError",
|
|
4106
|
+
severity: "error",
|
|
4107
|
+
category: "bridge",
|
|
4108
|
+
recoverable: true,
|
|
4109
|
+
message: \`No slot handler registered for \${service}.\${member}.\`,
|
|
4110
|
+
service,
|
|
4111
|
+
member,
|
|
4112
|
+
requestId,
|
|
4113
|
+
context: { interaction: "Slot" }
|
|
4114
|
+
});
|
|
4115
|
+
adapter.resolveSlot(requestId, false, undefined, "No slot handler registered.");
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
try {
|
|
4119
|
+
const result = await Promise.resolve(handler(...args));
|
|
4120
|
+
if (result instanceof Error) {
|
|
4121
|
+
this.reportFrontendDiagnostic({
|
|
4122
|
+
code: "SlotRequestFailed",
|
|
4123
|
+
severity: "error",
|
|
4124
|
+
category: "bridge",
|
|
4125
|
+
recoverable: true,
|
|
4126
|
+
message: result.message.length > 0
|
|
4127
|
+
? result.message
|
|
4128
|
+
: \`Slot \${service}.\${member} returned an Error.\`,
|
|
4129
|
+
service,
|
|
4130
|
+
member,
|
|
4131
|
+
requestId,
|
|
4132
|
+
context: { interaction: "Slot" }
|
|
4133
|
+
});
|
|
4134
|
+
adapter.resolveSlot(requestId, false, undefined, result.message);
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
adapter.resolveSlot(requestId, true, result, "");
|
|
4138
|
+
} catch (error) {
|
|
4139
|
+
const message = errorMessage(error);
|
|
4140
|
+
this.reportFrontendDiagnostic({
|
|
4141
|
+
code: "SlotHandlerError",
|
|
4142
|
+
severity: "error",
|
|
4143
|
+
category: "bridge",
|
|
4144
|
+
recoverable: true,
|
|
4145
|
+
message: \`Slot handler \${service}.\${member} threw: \${message}\`,
|
|
4146
|
+
service,
|
|
4147
|
+
member,
|
|
4148
|
+
requestId,
|
|
4149
|
+
context: { interaction: "Slot" }
|
|
4150
|
+
});
|
|
4151
|
+
adapter.resolveSlot(requestId, false, undefined, message);
|
|
4152
|
+
}
|
|
4153
|
+
});
|
|
4154
|
+
adapter.onDrop((service, member, payload, x, y) => {
|
|
4155
|
+
const key = this.key(service, member);
|
|
4156
|
+
for (const handler of this.dropHandlers.get(key) ?? []) {
|
|
4157
|
+
handler(payload, x, y);
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4160
|
+
adapter.onHover((service, member, payload, x, y) => {
|
|
4161
|
+
const key = this.key(service, member);
|
|
4162
|
+
for (const handler of this.hoverHandlers.get(key) ?? []) {
|
|
4163
|
+
handler(payload, x, y);
|
|
4164
|
+
}
|
|
4165
|
+
});
|
|
4166
|
+
adapter.onHoverLeft((service, member) => {
|
|
4167
|
+
const key = this.key(service, member);
|
|
4168
|
+
for (const handler of this.hoverLeftHandlers.get(key) ?? []) {
|
|
4169
|
+
handler();
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
for (const key of this.slotHandlers.keys()) {
|
|
4173
|
+
const parts = key.split("::");
|
|
4174
|
+
if (parts.length === 2) {
|
|
4175
|
+
adapter.registerSlot(parts[0], parts[1]);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
this._state.set("ready");
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
private key(service: string, member: string): string {
|
|
4182
|
+
return \`\${service}::\${member}\`;
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
class AnQstBridgeDiagnostics {
|
|
4187
|
+
constructor(private readonly _bridge: AnQstBridgeRuntime) {}
|
|
4188
|
+
|
|
4189
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
4190
|
+
return this._bridge.diagnostics();
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
state(): AnQstBridgeState {
|
|
4194
|
+
return this._bridge.state();
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
4198
|
+
return this._bridge.subscribeDiagnostics(listener);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
${serviceClasses}
|
|
4203
|
+
interface ${spec.widgetName}FrontendServices {
|
|
4204
|
+
${frontendServices}
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
interface ${spec.widgetName}Frontend {
|
|
4208
|
+
diagnostics: AnQstBridgeDiagnostics;
|
|
4209
|
+
services: ${spec.widgetName}FrontendServices;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
async function createFrontend(): Promise<${spec.widgetName}Frontend> {
|
|
4213
|
+
const bridge = new AnQstBridgeRuntime();
|
|
4214
|
+
await bridge.ready();
|
|
4215
|
+
return {
|
|
4216
|
+
diagnostics: new AnQstBridgeDiagnostics(bridge),
|
|
4217
|
+
services: {
|
|
4218
|
+
${frontendServiceFactories}
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
(function bootstrapAnQstGenerated(global: typeof globalThis & { AnQstGenerated?: { widgets?: Record<string, unknown> } }) {
|
|
4224
|
+
const root = global.AnQstGenerated ?? (global.AnQstGenerated = {});
|
|
4225
|
+
const widgets = root.widgets ?? (root.widgets = {});
|
|
4226
|
+
widgets["${spec.widgetName}"] = {
|
|
4227
|
+
createFrontend
|
|
4228
|
+
};
|
|
4229
|
+
})(window as typeof globalThis & { AnQstGenerated?: { widgets?: Record<string, unknown> } });
|
|
4230
|
+
`;
|
|
4231
|
+
}
|
|
4232
|
+
function renderVanillaServiceDts(spec, serviceName) {
|
|
4233
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
4234
|
+
const setMembers = [];
|
|
4235
|
+
const onSlotMembers = [];
|
|
4236
|
+
const classMembers = [];
|
|
4237
|
+
const setInterfaceName = `${serviceName}Set`;
|
|
4238
|
+
const onSlotInterfaceName = `${serviceName}OnSlot`;
|
|
4239
|
+
for (const m of members) {
|
|
4240
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
4241
|
+
if (m.kind === "Call") {
|
|
4242
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
4243
|
+
classMembers.push(` ${m.name}(${args}): Promise<${ret}>;`);
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
if (m.kind === "Emitter") {
|
|
4247
|
+
classMembers.push(` ${m.name}(${args}): void;`);
|
|
4248
|
+
continue;
|
|
4249
|
+
}
|
|
4250
|
+
if (m.kind === "Slot") {
|
|
4251
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
4252
|
+
onSlotMembers.push(` ${m.name}(handler: (${args}) => ${slotHandlerReturnType(ret)}): void;`);
|
|
4253
|
+
continue;
|
|
4254
|
+
}
|
|
4255
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
4256
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4257
|
+
classMembers.push(` ${m.name}(): ${tsType} | undefined;`);
|
|
4258
|
+
if (m.kind === "Input") {
|
|
4259
|
+
setMembers.push(` ${m.name}(value: ${tsType}): void;`);
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
4263
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4264
|
+
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
4265
|
+
}
|
|
4266
|
+
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
4267
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4268
|
+
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
const interfaceBlocks = [];
|
|
4272
|
+
if (setMembers.length > 0) {
|
|
4273
|
+
interfaceBlocks.push(`interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`);
|
|
4274
|
+
}
|
|
4275
|
+
if (onSlotMembers.length > 0) {
|
|
4276
|
+
interfaceBlocks.push(`interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`);
|
|
4277
|
+
}
|
|
4278
|
+
const interfaceSection = interfaceBlocks.length > 0 ? `${interfaceBlocks.join("\n\n")}\n\n` : "";
|
|
4279
|
+
const namespaceLines = [];
|
|
4280
|
+
if (setMembers.length > 0) {
|
|
4281
|
+
namespaceLines.push(` readonly set: ${setInterfaceName};`);
|
|
4282
|
+
}
|
|
4283
|
+
if (onSlotMembers.length > 0) {
|
|
4284
|
+
namespaceLines.push(` readonly onSlot: ${onSlotInterfaceName};`);
|
|
4285
|
+
}
|
|
4286
|
+
const declareBodyLines = [...namespaceLines, ...classMembers];
|
|
4287
|
+
return `${interfaceSection}declare class ${serviceName} {
|
|
4288
|
+
${declareBodyLines.join("\n")}
|
|
4289
|
+
}`;
|
|
4290
|
+
}
|
|
4291
|
+
function renderVanillaIndexDts(spec) {
|
|
4292
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}/index.d.ts`).trim();
|
|
4293
|
+
// Export widget namespace types so other packages can `import type { ... }` from this declaration file
|
|
4294
|
+
// (e.g. a second widget spec that reuses structs generated for the first widget).
|
|
4295
|
+
const localTypeDecls = renderTypeDeclarations(spec, true).trim();
|
|
4296
|
+
const serviceDecls = spec.services.map((s) => renderVanillaServiceDts(spec, s.name)).join("\n\n");
|
|
4297
|
+
const servicesShape = spec.services.length > 0
|
|
4298
|
+
? spec.services.map((s) => ` ${s.name}: ${s.name};`).join("\n")
|
|
4299
|
+
: "";
|
|
4300
|
+
const sections = [externalTypeImports, localTypeDecls].filter((s) => s.length > 0);
|
|
4301
|
+
const prelude = sections.length > 0 ? `${sections.join("\n\n")}\n\n` : "";
|
|
4302
|
+
return `export {};
|
|
4303
|
+
${prelude}type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
4304
|
+
|
|
4305
|
+
type AnQstBridgeSource = "frontend" | "host";
|
|
4306
|
+
|
|
4307
|
+
type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
4308
|
+
|
|
4309
|
+
type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
4310
|
+
|
|
4311
|
+
interface AnQstBridgeDiagnostic {
|
|
4312
|
+
code: string;
|
|
4313
|
+
severity: AnQstBridgeSeverity;
|
|
4314
|
+
category: string;
|
|
4315
|
+
recoverable: boolean;
|
|
4316
|
+
message: string;
|
|
4317
|
+
timestamp: string;
|
|
4318
|
+
source: AnQstBridgeSource;
|
|
4319
|
+
transport?: AnQstBridgeTransport;
|
|
4320
|
+
service?: string;
|
|
4321
|
+
member?: string;
|
|
4322
|
+
requestId?: string;
|
|
4323
|
+
context?: Record<string, unknown>;
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
declare class AnQstBridgeDiagnostics {
|
|
4327
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[];
|
|
4328
|
+
state(): AnQstBridgeState;
|
|
4329
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
${serviceDecls}
|
|
4333
|
+
|
|
4334
|
+
interface ${spec.widgetName}FrontendServices {
|
|
4335
|
+
${servicesShape}
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
interface ${spec.widgetName}Frontend {
|
|
4339
|
+
diagnostics: AnQstBridgeDiagnostics;
|
|
4340
|
+
services: ${spec.widgetName}FrontendServices;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
interface ${spec.widgetName}Global {
|
|
4344
|
+
createFrontend(): Promise<${spec.widgetName}Frontend>;
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
interface AnQstGeneratedWidgets {
|
|
4348
|
+
${spec.widgetName}: ${spec.widgetName}Global;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
interface AnQstGeneratedRoot {
|
|
4352
|
+
widgets: AnQstGeneratedWidgets;
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
declare global {
|
|
4356
|
+
interface Window {
|
|
4357
|
+
AnQstGenerated: AnQstGeneratedRoot;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
var AnQstGenerated: AnQstGeneratedRoot;
|
|
4361
|
+
}
|
|
4362
|
+
`;
|
|
4363
|
+
}
|
|
4364
|
+
function transpileBrowserTsToJs(source) {
|
|
4365
|
+
return typescript_1.default.transpileModule(source, {
|
|
4366
|
+
compilerOptions: {
|
|
4367
|
+
target: typescript_1.default.ScriptTarget.ES2018,
|
|
4368
|
+
module: typescript_1.default.ModuleKind.None,
|
|
4369
|
+
importsNotUsedAsValues: typescript_1.default.ImportsNotUsedAsValues.Remove
|
|
4370
|
+
}
|
|
4371
|
+
}).outputText;
|
|
4372
|
+
}
|
|
4373
|
+
function renderVanillaPackage(spec, target) {
|
|
4374
|
+
const packageJson = {
|
|
4375
|
+
name: `${spec.widgetName.toLowerCase()}-${target.toLowerCase()}-generated`,
|
|
4376
|
+
version: "0.1.0",
|
|
4377
|
+
private: true,
|
|
4378
|
+
main: "index.js",
|
|
4379
|
+
anqst: {
|
|
4380
|
+
widget: spec.widgetName,
|
|
4381
|
+
services: spec.services.map((s) => s.name),
|
|
4382
|
+
target,
|
|
4383
|
+
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport,
|
|
4384
|
+
outputContractVersion: 2
|
|
4385
|
+
}
|
|
4386
|
+
};
|
|
4387
|
+
if (target === "VanillaTS") {
|
|
4388
|
+
packageJson.types = "index.d.ts";
|
|
4389
|
+
}
|
|
4390
|
+
return JSON.stringify(packageJson, null, 2);
|
|
4391
|
+
}
|
|
4392
|
+
function renderNodeExpressWsPackage(spec) {
|
|
4393
|
+
return JSON.stringify({
|
|
4394
|
+
name: `${spec.widgetName.toLowerCase()}-node-express-ws-generated`,
|
|
4395
|
+
version: "0.1.0",
|
|
4396
|
+
private: true,
|
|
4397
|
+
types: "types/index.d.ts",
|
|
4398
|
+
main: "index.ts",
|
|
4399
|
+
exports: {
|
|
4400
|
+
".": {
|
|
4401
|
+
types: "./types/index.d.ts",
|
|
4402
|
+
default: "./index.ts"
|
|
4403
|
+
}
|
|
4404
|
+
},
|
|
4405
|
+
anqst: {
|
|
4406
|
+
widget: spec.widgetName,
|
|
4407
|
+
services: spec.services.map((s) => s.name),
|
|
4408
|
+
target: "node_express_ws"
|
|
4409
|
+
}
|
|
4410
|
+
}, null, 2);
|
|
4411
|
+
}
|
|
4412
|
+
function nodeParamTuple(member) {
|
|
4413
|
+
if (member.parameters.length === 0)
|
|
4414
|
+
return "[]";
|
|
4415
|
+
return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
|
|
4416
|
+
}
|
|
4417
|
+
function nodeParamArgs(member) {
|
|
4418
|
+
return member.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
4419
|
+
}
|
|
4420
|
+
function nodeParamValues(member) {
|
|
4421
|
+
if (member.parameters.length === 0)
|
|
4422
|
+
return "[]";
|
|
4423
|
+
return `[${member.parameters.map((p) => p.name).join(", ")}]`;
|
|
4424
|
+
}
|
|
4425
|
+
function nodeCap(value) {
|
|
4426
|
+
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
4427
|
+
}
|
|
4428
|
+
function renderNodeExpressWsTypes(spec) {
|
|
4429
|
+
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
|
|
4430
|
+
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
4431
|
+
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
4432
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
4433
|
+
}
|
|
4434
|
+
function renderNodeExpressWsIndex(spec, codecCatalog) {
|
|
4435
|
+
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
|
|
4436
|
+
const typeDecls = renderTypeDeclarations(spec, true);
|
|
4437
|
+
const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
|
|
4438
|
+
const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
|
|
4439
|
+
const handlerInterfaces = spec.services
|
|
4440
|
+
.map((service) => {
|
|
4441
|
+
const lines = [];
|
|
4442
|
+
for (const member of service.members) {
|
|
4443
|
+
const args = nodeParamArgs(member);
|
|
4444
|
+
const prefixedArgs = args.length > 0 ? `, ${args}` : "";
|
|
4445
|
+
if (member.kind === "Call" && member.payloadTypeText) {
|
|
4446
|
+
const ret = mapTypeTextToTs(member.payloadTypeText);
|
|
4447
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): ${ret} | Promise<${ret}>;`);
|
|
4448
|
+
}
|
|
4449
|
+
else if (member.kind === "Emitter") {
|
|
4450
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): void | Promise<void>;`);
|
|
4451
|
+
}
|
|
4452
|
+
else if (member.kind === "Input" && member.payloadTypeText) {
|
|
4453
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}, value: ${mapTypeTextToTs(member.payloadTypeText)}): void | Promise<void>;`);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
return `export interface ${service.name}NodeHandlers {\n${lines.join("\n")}\n}`;
|
|
4457
|
+
})
|
|
4458
|
+
.join("\n\n");
|
|
2746
4459
|
const implementationFields = spec.services.map((service) => ` ${service.name}: ${service.name}NodeHandlers;`).join("\n");
|
|
2747
4460
|
const slotHelpers = spec.services
|
|
2748
4461
|
.flatMap((service) => service.members
|
|
@@ -2750,8 +4463,8 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2750
4463
|
.map((member) => {
|
|
2751
4464
|
const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
|
|
2752
4465
|
const args = nodeParamArgs(member);
|
|
2753
|
-
const paramSites = member.parameters.map((p) => (0,
|
|
2754
|
-
const payloadSite = (0,
|
|
4466
|
+
const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
|
|
4467
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
2755
4468
|
const encodedArgs = member.parameters.length > 0
|
|
2756
4469
|
? `[${member.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
2757
4470
|
: "[]";
|
|
@@ -2765,7 +4478,7 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2765
4478
|
.filter((member) => member.kind === "Output" && member.payloadTypeText)
|
|
2766
4479
|
.map((member) => {
|
|
2767
4480
|
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
2768
|
-
const payloadSite = (0,
|
|
4481
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
2769
4482
|
return ` set${service.name}_${nodeCap(member.name)}(value: ${typeText}): void {
|
|
2770
4483
|
this.setOutputValue("${service.name}", "${member.name}", ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"});
|
|
2771
4484
|
}`;
|
|
@@ -2820,7 +4533,7 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2820
4533
|
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
2821
4534
|
.map((member) => {
|
|
2822
4535
|
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
2823
|
-
const payloadSite = (0,
|
|
4536
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
2824
4537
|
if (member.kind === "Input") {
|
|
2825
4538
|
return ` ${member.name}: {\n get: () => session.readInput("${service.name}", "${member.name}").then((value) => ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${typeText}`}),\n on: (handler: (value: ${typeText}) => void) => session.onInput("${service.name}", "${member.name}", (value) => handler(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${typeText}`}))\n },`;
|
|
2826
4539
|
}
|
|
@@ -2834,8 +4547,8 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2834
4547
|
.flatMap((service) => service.members
|
|
2835
4548
|
.filter((member) => member.kind === "Call" && member.payloadTypeText)
|
|
2836
4549
|
.map((member) => {
|
|
2837
|
-
const paramSites = member.parameters.map((p) => (0,
|
|
2838
|
-
const payloadSite = (0,
|
|
4550
|
+
const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
|
|
4551
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
2839
4552
|
const decodedArgs = member.parameters.length > 0
|
|
2840
4553
|
? member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")
|
|
2841
4554
|
: "";
|
|
@@ -2890,7 +4603,7 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2890
4603
|
.flatMap((service) => service.members
|
|
2891
4604
|
.filter((member) => member.kind === "Emitter")
|
|
2892
4605
|
.map((member) => {
|
|
2893
|
-
const paramSites = member.parameters.map((p) => (0,
|
|
4606
|
+
const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
|
|
2894
4607
|
const decodedArgs = member.parameters.length > 0
|
|
2895
4608
|
? member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")
|
|
2896
4609
|
: "";
|
|
@@ -2935,7 +4648,7 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2935
4648
|
.flatMap((service) => service.members
|
|
2936
4649
|
.filter((member) => member.kind === "Input" && member.payloadTypeText)
|
|
2937
4650
|
.map((member) => {
|
|
2938
|
-
const payloadSite = (0,
|
|
4651
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
2939
4652
|
return ` if (service === "${service.name}" && member === "${member.name}") {
|
|
2940
4653
|
const handler = implementation.${service.name}.${member.name};
|
|
2941
4654
|
if (typeof handler !== "function") {
|
|
@@ -2976,8 +4689,8 @@ import type { WebSocket, WebSocketServer } from "ws";
|
|
|
2976
4689
|
${typeImports}
|
|
2977
4690
|
${typeDecls}
|
|
2978
4691
|
|
|
2979
|
-
//
|
|
2980
|
-
${(0,
|
|
4692
|
+
// Boundary codec plan helpers
|
|
4693
|
+
${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
|
|
2981
4694
|
|
|
2982
4695
|
${handlerInterfaces}
|
|
2983
4696
|
|
|
@@ -3439,7 +5152,23 @@ function renderTypeRootIndexDts(spec) {
|
|
|
3439
5152
|
const typeDecls = renderTypeTypesDts(spec).trim();
|
|
3440
5153
|
const serviceDecls = renderTypeServicesDts(spec).trim();
|
|
3441
5154
|
const sections = [indexDecls, typeDecls, serviceDecls].filter((s) => s.length > 0);
|
|
3442
|
-
|
|
5155
|
+
if (sections.length === 0)
|
|
5156
|
+
return "";
|
|
5157
|
+
const dedupedLines = [];
|
|
5158
|
+
const seenImportLines = new Set();
|
|
5159
|
+
for (const line of sections.join("\n\n").split("\n")) {
|
|
5160
|
+
const trimmed = line.trim();
|
|
5161
|
+
if (!trimmed.startsWith("import type ")) {
|
|
5162
|
+
dedupedLines.push(line);
|
|
5163
|
+
continue;
|
|
5164
|
+
}
|
|
5165
|
+
if (seenImportLines.has(trimmed))
|
|
5166
|
+
continue;
|
|
5167
|
+
seenImportLines.add(trimmed);
|
|
5168
|
+
dedupedLines.push(line);
|
|
5169
|
+
}
|
|
5170
|
+
const deduped = dedupedLines.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
5171
|
+
return `${deduped}\n`;
|
|
3443
5172
|
}
|
|
3444
5173
|
function generatedCppLibraryDirName(widgetName) {
|
|
3445
5174
|
return (0, layout_1.generatedQtWidgetDirName)(widgetName);
|
|
@@ -3447,35 +5176,59 @@ function generatedCppLibraryDirName(widgetName) {
|
|
|
3447
5176
|
function generatedNodeExpressWsDirName(widgetName) {
|
|
3448
5177
|
return (0, layout_1.generatedNodeExpressDirName)(widgetName);
|
|
3449
5178
|
}
|
|
3450
|
-
function generateOutputs(spec, options = {
|
|
3451
|
-
const
|
|
5179
|
+
function generateOutputs(spec, options = {}) {
|
|
5180
|
+
const useDefaultBrowserTargets = Object.keys(options).length === 0;
|
|
5181
|
+
const normalizedOptions = {
|
|
5182
|
+
emitQWidget: options.emitQWidget ?? true,
|
|
5183
|
+
emitAngularService: options.emitAngularService ?? true,
|
|
5184
|
+
emitVanillaTS: options.emitVanillaTS ?? useDefaultBrowserTargets,
|
|
5185
|
+
emitVanillaJS: options.emitVanillaJS ?? useDefaultBrowserTargets,
|
|
5186
|
+
emitNodeExpressWs: options.emitNodeExpressWs ?? false
|
|
5187
|
+
};
|
|
5188
|
+
const angularFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}`;
|
|
5189
|
+
const vanillaTsFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}`;
|
|
5190
|
+
const vanillaJsFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaJS")}`;
|
|
3452
5191
|
const cppDir = `backend/cpp/qt/${generatedCppLibraryDirName(spec.widgetName)}`;
|
|
3453
5192
|
const nodeDir = `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}`;
|
|
3454
5193
|
const outputs = {};
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
outputs[`${
|
|
3458
|
-
outputs[`${
|
|
3459
|
-
outputs[`${
|
|
3460
|
-
outputs[`${
|
|
3461
|
-
outputs[`${
|
|
3462
|
-
outputs[`${
|
|
3463
|
-
outputs[`${
|
|
3464
|
-
outputs[`${
|
|
3465
|
-
outputs[`${
|
|
3466
|
-
|
|
3467
|
-
|
|
5194
|
+
const codecCatalog = (0, boundary_codecs_1.buildBoundaryCodecCatalog)(spec);
|
|
5195
|
+
if (normalizedOptions.emitAngularService) {
|
|
5196
|
+
outputs[`${angularFrontendDir}/package.json`] = renderNpmPackage(spec);
|
|
5197
|
+
outputs[`${angularFrontendDir}/index.ts`] = renderTsIndex();
|
|
5198
|
+
outputs[`${angularFrontendDir}/services.ts`] = renderTsServices(spec, codecCatalog);
|
|
5199
|
+
outputs[`${angularFrontendDir}/types.ts`] = renderTsTypes(spec);
|
|
5200
|
+
outputs[`${angularFrontendDir}/index.js`] = renderJsIndex();
|
|
5201
|
+
outputs[`${angularFrontendDir}/services.js`] = renderJsServices();
|
|
5202
|
+
outputs[`${angularFrontendDir}/types.js`] = renderJsTypes();
|
|
5203
|
+
outputs[`${angularFrontendDir}/types/index.d.ts`] = renderTypeRootIndexDts(spec);
|
|
5204
|
+
outputs[`${angularFrontendDir}/types/services.d.ts`] = renderTypeServicesDts(spec);
|
|
5205
|
+
outputs[`${angularFrontendDir}/types/types.d.ts`] = renderTypeTypesDts(spec);
|
|
5206
|
+
}
|
|
5207
|
+
if (normalizedOptions.emitVanillaTS || normalizedOptions.emitVanillaJS) {
|
|
5208
|
+
const vanillaBrowserTs = renderVanillaBrowserTs(spec, codecCatalog);
|
|
5209
|
+
const vanillaBrowserJs = transpileBrowserTsToJs(vanillaBrowserTs);
|
|
5210
|
+
if (normalizedOptions.emitVanillaTS) {
|
|
5211
|
+
outputs[`${vanillaTsFrontendDir}/package.json`] = renderVanillaPackage(spec, "VanillaTS");
|
|
5212
|
+
outputs[`${vanillaTsFrontendDir}/index.js`] = vanillaBrowserJs;
|
|
5213
|
+
outputs[`${vanillaTsFrontendDir}/index.d.ts`] = renderVanillaIndexDts(spec);
|
|
5214
|
+
}
|
|
5215
|
+
if (normalizedOptions.emitVanillaJS) {
|
|
5216
|
+
outputs[`${vanillaJsFrontendDir}/package.json`] = renderVanillaPackage(spec, "VanillaJS");
|
|
5217
|
+
outputs[`${vanillaJsFrontendDir}/index.js`] = vanillaBrowserJs;
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
if (normalizedOptions.emitQWidget) {
|
|
3468
5221
|
const cppTypes = buildCppTypeContext(spec);
|
|
3469
5222
|
outputs[`${cppDir}/CMakeLists.txt`] = renderCMake(spec);
|
|
3470
5223
|
outputs[`${cppDir}/${spec.widgetName}.qrc`] = renderEmbeddedQrc(spec.widgetName, []);
|
|
3471
5224
|
outputs[`${cppDir}/include/${spec.widgetName}.h`] = renderWidgetUmbrellaHeader(spec);
|
|
3472
|
-
outputs[`${cppDir}/include/${spec.widgetName}Widget.h`] = renderWidgetHeader(spec, cppTypes);
|
|
5225
|
+
outputs[`${cppDir}/include/${spec.widgetName}Widget.h`] = renderWidgetHeader(spec, cppTypes, codecCatalog);
|
|
3473
5226
|
outputs[`${cppDir}/include/${spec.widgetName}Types.h`] = renderTypesHeader(spec, cppTypes);
|
|
3474
|
-
outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes);
|
|
5227
|
+
outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes, codecCatalog);
|
|
3475
5228
|
}
|
|
3476
|
-
if (
|
|
5229
|
+
if (normalizedOptions.emitNodeExpressWs) {
|
|
3477
5230
|
outputs[`${nodeDir}/package.json`] = renderNodeExpressWsPackage(spec);
|
|
3478
|
-
outputs[`${nodeDir}/index.ts`] = renderNodeExpressWsIndex(spec);
|
|
5231
|
+
outputs[`${nodeDir}/index.ts`] = renderNodeExpressWsIndex(spec, codecCatalog);
|
|
3479
5232
|
outputs[`${nodeDir}/types/index.d.ts`] = renderNodeExpressWsTypes(spec);
|
|
3480
5233
|
}
|
|
3481
5234
|
return outputs;
|