@dusted/anqst 1.0.1 → 1.5.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 +15 -0
- package/dist/src/app.js +1 -0
- package/dist/src/base93.js +124 -0
- package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/decoder.js +35 -0
- package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/encoder.js +36 -0
- package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/decoder.js +26 -0
- package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/encoder.js +38 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-blob/decoder.js +28 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-blob/encoder.js +34 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-buffer/decoder.js +29 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-buffer/encoder.js +36 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/decoder.js +46 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/encoder.js +49 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/decoder.js +46 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/encoder.js +47 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/decoder.js +46 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/encoder.js +49 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/decoder.js +50 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/encoder.js +52 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/decoder.js +38 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/encoder.js +44 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/decoder.js +33 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/encoder.js +34 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/decoder.js +46 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/encoder.js +49 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/decoder.js +46 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/encoder.js +49 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/decoder.js +28 -0
- package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/encoder.js +34 -0
- package/dist/src/codecgenerators/basecodecemitters/boolean/decoder.js +34 -0
- package/dist/src/codecgenerators/basecodecemitters/boolean/encoder.js +40 -0
- package/dist/src/codecgenerators/basecodecemitters/dynamic-json/decoder.js +43 -0
- package/dist/src/codecgenerators/basecodecemitters/dynamic-json/encoder.js +45 -0
- package/dist/src/codecgenerators/basecodecemitters/dynamic-object/decoder.js +44 -0
- package/dist/src/codecgenerators/basecodecemitters/dynamic-object/encoder.js +46 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-int16/decoder.js +32 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-int16/encoder.js +43 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-int32/decoder.js +26 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-int32/encoder.js +37 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-int8/decoder.js +26 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-int8/encoder.js +37 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-qint16/decoder.js +36 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-qint16/encoder.js +36 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-qint32/decoder.js +25 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-qint32/encoder.js +36 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-qint8/decoder.js +36 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-qint8/encoder.js +36 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-quint16/decoder.js +26 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-quint16/encoder.js +38 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-quint32/decoder.js +27 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-quint32/encoder.js +39 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-quint8/decoder.js +26 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-quint8/encoder.js +38 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-uint16/decoder.js +30 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-uint16/encoder.js +42 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-uint32/decoder.js +31 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-uint32/encoder.js +43 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-uint8/decoder.js +30 -0
- package/dist/src/codecgenerators/basecodecemitters/integer-uint8/encoder.js +40 -0
- package/dist/src/codecgenerators/basecodecemitters/number/decoder.js +26 -0
- package/dist/src/codecgenerators/basecodecemitters/number/encoder.js +38 -0
- package/dist/src/codecgenerators/basecodecemitters/shared/comments.js +13 -0
- package/dist/src/codecgenerators/basecodecemitters/shared/contracts.js +2 -0
- package/dist/src/codecgenerators/basecodecemitters/shared/fixedwidth.js +53 -0
- package/dist/src/codecgenerators/basecodecemitters/shared/index.js +21 -0
- package/dist/src/codecgenerators/basecodecemitters/shared/positionalBase93.js +48 -0
- package/dist/src/codecgenerators/basecodecemitters/shared/rawbytes.js +30 -0
- package/dist/src/codecgenerators/basecodecemitters/string/decoder.js +43 -0
- package/dist/src/codecgenerators/basecodecemitters/string/encoder.js +43 -0
- package/dist/src/codecgenerators/basecodecemitters/stringArray/decoder.js +80 -0
- package/dist/src/codecgenerators/basecodecemitters/stringArray/encoder.js +57 -0
- package/dist/src/emit.js +752 -118
- package/dist/src/structured-top-level-codecs.js +1305 -0
- package/package.json +1 -1
- package/spec/AnQst-Spec-DSL.d.ts +12 -6
package/dist/src/emit.js
CHANGED
|
@@ -13,6 +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 structured_top_level_codecs_1 = require("./structured-top-level-codecs");
|
|
16
17
|
function stripAnQstType(typeText) {
|
|
17
18
|
return typeText
|
|
18
19
|
.replace(/\bAnQst\.Type\.stringArray\b/g, "string[]")
|
|
@@ -22,6 +23,27 @@ function stripAnQstType(typeText) {
|
|
|
22
23
|
.replace(/\bAnQst\.Type\.quint64\b/g, "bigint")
|
|
23
24
|
.replace(/\bAnQst\.Type\.qint32\b/g, "number")
|
|
24
25
|
.replace(/\bAnQst\.Type\.quint32\b/g, "number")
|
|
26
|
+
.replace(/\bAnQst\.Type\.qint16\b/g, "number")
|
|
27
|
+
.replace(/\bAnQst\.Type\.quint16\b/g, "number")
|
|
28
|
+
.replace(/\bAnQst\.Type\.qint8\b/g, "number")
|
|
29
|
+
.replace(/\bAnQst\.Type\.quint8\b/g, "number")
|
|
30
|
+
.replace(/\bAnQst\.Type\.int32\b/g, "number")
|
|
31
|
+
.replace(/\bAnQst\.Type\.uint32\b/g, "number")
|
|
32
|
+
.replace(/\bAnQst\.Type\.int16\b/g, "number")
|
|
33
|
+
.replace(/\bAnQst\.Type\.uint16\b/g, "number")
|
|
34
|
+
.replace(/\bAnQst\.Type\.int8\b/g, "number")
|
|
35
|
+
.replace(/\bAnQst\.Type\.uint8\b/g, "number")
|
|
36
|
+
.replace(/\bAnQst\.Type\.buffer\b/g, "ArrayBuffer")
|
|
37
|
+
.replace(/\bAnQst\.Type\.blob\b/g, "ArrayBuffer")
|
|
38
|
+
.replace(/\bAnQst\.Type\.typedArray\b/g, "Uint8Array")
|
|
39
|
+
.replace(/\bAnQst\.Type\.uint8Array\b/g, "Uint8Array")
|
|
40
|
+
.replace(/\bAnQst\.Type\.int8Array\b/g, "Int8Array")
|
|
41
|
+
.replace(/\bAnQst\.Type\.uint16Array\b/g, "Uint16Array")
|
|
42
|
+
.replace(/\bAnQst\.Type\.int16Array\b/g, "Int16Array")
|
|
43
|
+
.replace(/\bAnQst\.Type\.uint32Array\b/g, "Uint32Array")
|
|
44
|
+
.replace(/\bAnQst\.Type\.int32Array\b/g, "Int32Array")
|
|
45
|
+
.replace(/\bAnQst\.Type\.float32Array\b/g, "Float32Array")
|
|
46
|
+
.replace(/\bAnQst\.Type\.float64Array\b/g, "Float64Array")
|
|
25
47
|
.replace(/\bAnQst\.Type\.object\b/g, "object")
|
|
26
48
|
.replace(/\bAnQst\.Type\.json\b/g, "object");
|
|
27
49
|
}
|
|
@@ -31,6 +53,36 @@ function splitGeneric(typeText) {
|
|
|
31
53
|
return null;
|
|
32
54
|
return { name: m[1], arg: m[2].trim() };
|
|
33
55
|
}
|
|
56
|
+
function filterNullishUnionTypeNodes(types) {
|
|
57
|
+
return types.filter((part) => part.kind !== typescript_1.default.SyntaxKind.NullKeyword && part.kind !== typescript_1.default.SyntaxKind.UndefinedKeyword);
|
|
58
|
+
}
|
|
59
|
+
function isStringLikeUnionTypeNode(node) {
|
|
60
|
+
return node.types.every((part) => {
|
|
61
|
+
if (typescript_1.default.isLiteralTypeNode(part) && typescript_1.default.isStringLiteral(part.literal))
|
|
62
|
+
return true;
|
|
63
|
+
if (part.kind === typescript_1.default.SyntaxKind.StringKeyword)
|
|
64
|
+
return true;
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function isBooleanLikeUnionTypeNode(node) {
|
|
69
|
+
return node.types.every((part) => {
|
|
70
|
+
if (typescript_1.default.isLiteralTypeNode(part) && (part.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword || part.literal.kind === typescript_1.default.SyntaxKind.FalseKeyword))
|
|
71
|
+
return true;
|
|
72
|
+
if (part.kind === typescript_1.default.SyntaxKind.BooleanKeyword)
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function isNumberLikeUnionTypeNode(node) {
|
|
78
|
+
return node.types.every((part) => {
|
|
79
|
+
if (typescript_1.default.isLiteralTypeNode(part) && typescript_1.default.isNumericLiteral(part.literal))
|
|
80
|
+
return true;
|
|
81
|
+
if (part.kind === typescript_1.default.SyntaxKind.NumberKeyword || part.kind === typescript_1.default.SyntaxKind.BigIntKeyword)
|
|
82
|
+
return true;
|
|
83
|
+
return false;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
34
86
|
function mapTsTypeToCpp(typeText) {
|
|
35
87
|
const raw = typeText.trim();
|
|
36
88
|
if (/\bAnQst\.Type\.qint64\b/.test(raw))
|
|
@@ -55,6 +107,9 @@ function mapTsTypeToCpp(typeText) {
|
|
|
55
107
|
return "QString";
|
|
56
108
|
if (/\bAnQst\.Type\.json\b/.test(raw) || /\bAnQst\.Type\.object\b/.test(raw))
|
|
57
109
|
return "QVariantMap";
|
|
110
|
+
if (/\bAnQst\.Type\.(?:buffer|blob|typedArray|uint8Array|int8Array|uint16Array|int16Array|uint32Array|int32Array|float32Array|float64Array)\b/.test(raw)) {
|
|
111
|
+
return "QByteArray";
|
|
112
|
+
}
|
|
58
113
|
if (/\bAnQst\.Type\.(u?int(8|16|32))\b/.test(raw)) {
|
|
59
114
|
const narrowed = raw.match(/\bAnQst\.Type\.(u?int(?:8|16|32))\b/)?.[1];
|
|
60
115
|
if (narrowed === "int8")
|
|
@@ -83,6 +138,20 @@ function mapTsTypeToCpp(typeText) {
|
|
|
83
138
|
return "void";
|
|
84
139
|
if (t === "object")
|
|
85
140
|
return "QVariantMap";
|
|
141
|
+
if (t === "ArrayBuffer")
|
|
142
|
+
return "QByteArray";
|
|
143
|
+
if ([
|
|
144
|
+
"Uint8Array",
|
|
145
|
+
"Int8Array",
|
|
146
|
+
"Uint16Array",
|
|
147
|
+
"Int16Array",
|
|
148
|
+
"Uint32Array",
|
|
149
|
+
"Int32Array",
|
|
150
|
+
"Float32Array",
|
|
151
|
+
"Float64Array"
|
|
152
|
+
].includes(t)) {
|
|
153
|
+
return "QByteArray";
|
|
154
|
+
}
|
|
86
155
|
if (t.endsWith("[]")) {
|
|
87
156
|
return `QList<${mapTsTypeToCpp(t.slice(0, -2))}>`;
|
|
88
157
|
}
|
|
@@ -117,6 +186,8 @@ function variantToCppExpression(cppType, expr) {
|
|
|
117
186
|
return `${expr}.toStringList()`;
|
|
118
187
|
if (cppType === "QVariantMap")
|
|
119
188
|
return `${expr}.toMap()`;
|
|
189
|
+
if (cppType === "QByteArray")
|
|
190
|
+
return `${expr}.toByteArray()`;
|
|
120
191
|
if (cppType === "double")
|
|
121
192
|
return `${expr}.toDouble()`;
|
|
122
193
|
if (cppType === "bool")
|
|
@@ -140,6 +211,7 @@ function cppToVariantExpression(cppType, expr) {
|
|
|
140
211
|
if (cppType === "QString" ||
|
|
141
212
|
cppType === "QStringList" ||
|
|
142
213
|
cppType === "QVariantMap" ||
|
|
214
|
+
cppType === "QByteArray" ||
|
|
143
215
|
cppType === "double" ||
|
|
144
216
|
cppType === "bool" ||
|
|
145
217
|
cppType === "qint64" ||
|
|
@@ -398,6 +470,7 @@ class CppTypeNormalizer {
|
|
|
398
470
|
this.seedOrder = [];
|
|
399
471
|
this.allKnownNames = new Set();
|
|
400
472
|
this.usedNames = new Set();
|
|
473
|
+
this.syntheticNameByKey = new Map();
|
|
401
474
|
for (const decl of collectStructDecls(spec)) {
|
|
402
475
|
this.allKnownNames.add(decl.name);
|
|
403
476
|
this.usedNames.add(decl.name);
|
|
@@ -459,6 +532,16 @@ class CppTypeNormalizer {
|
|
|
459
532
|
return this.mapTypeNode(typeNode.type, nameHintParts, deps);
|
|
460
533
|
}
|
|
461
534
|
if (typescript_1.default.isUnionTypeNode(typeNode)) {
|
|
535
|
+
const filtered = filterNullishUnionTypeNodes(typeNode.types);
|
|
536
|
+
if (filtered.length === 1) {
|
|
537
|
+
return this.mapTypeNode(filtered[0], nameHintParts, deps);
|
|
538
|
+
}
|
|
539
|
+
if (isStringLikeUnionTypeNode(typeNode))
|
|
540
|
+
return "QString";
|
|
541
|
+
if (isBooleanLikeUnionTypeNode(typeNode))
|
|
542
|
+
return "bool";
|
|
543
|
+
if (isNumberLikeUnionTypeNode(typeNode))
|
|
544
|
+
return "double";
|
|
462
545
|
return "QString";
|
|
463
546
|
}
|
|
464
547
|
if (typescript_1.default.isTypeLiteralNode(typeNode)) {
|
|
@@ -506,7 +589,14 @@ class CppTypeNormalizer {
|
|
|
506
589
|
}
|
|
507
590
|
ensureSyntheticStruct(typeNode, nameHintParts, deps) {
|
|
508
591
|
const baseName = this.makeSyntheticBaseName(nameHintParts);
|
|
592
|
+
const syntheticKey = `${baseName}::${typeNode.getText()}`;
|
|
593
|
+
const existingName = this.syntheticNameByKey.get(syntheticKey);
|
|
594
|
+
if (existingName) {
|
|
595
|
+
deps.add(existingName);
|
|
596
|
+
return existingName;
|
|
597
|
+
}
|
|
509
598
|
const synthesizedName = this.allocateUniqueName(baseName);
|
|
599
|
+
this.syntheticNameByKey.set(syntheticKey, synthesizedName);
|
|
510
600
|
if (this.declMap.has(synthesizedName)) {
|
|
511
601
|
deps.add(synthesizedName);
|
|
512
602
|
return synthesizedName;
|
|
@@ -647,6 +737,29 @@ function collectDragDropMimeConstants(spec) {
|
|
|
647
737
|
}
|
|
648
738
|
return constants;
|
|
649
739
|
}
|
|
740
|
+
function collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog) {
|
|
741
|
+
const seen = new Set();
|
|
742
|
+
const helpers = [];
|
|
743
|
+
for (const service of spec.services) {
|
|
744
|
+
for (const member of service.members) {
|
|
745
|
+
if ((member.kind !== "DropTarget" && member.kind !== "HoverTarget") || !member.payloadTypeText)
|
|
746
|
+
continue;
|
|
747
|
+
const typeName = member.payloadTypeText.replace(/\s/g, "");
|
|
748
|
+
if (seen.has(typeName))
|
|
749
|
+
continue;
|
|
750
|
+
seen.add(typeName);
|
|
751
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
752
|
+
if (!payloadSite)
|
|
753
|
+
continue;
|
|
754
|
+
helpers.push({
|
|
755
|
+
typeName,
|
|
756
|
+
cppType: cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]),
|
|
757
|
+
codecId: payloadSite.codecId
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return helpers;
|
|
762
|
+
}
|
|
650
763
|
function renderTypesHeader(spec, cppTypes) {
|
|
651
764
|
const decls = cppTypes.orderedDecls.map(renderCppDecl).join("\n\n");
|
|
652
765
|
const metatypes = cppTypes.structNames
|
|
@@ -663,6 +776,7 @@ function renderTypesHeader(spec, cppTypes) {
|
|
|
663
776
|
return `#pragma once
|
|
664
777
|
#include <QString>
|
|
665
778
|
#include <QStringList>
|
|
779
|
+
#include <QByteArray>
|
|
666
780
|
#include <QList>
|
|
667
781
|
#include <QVariantMap>
|
|
668
782
|
#include <QMetaType>
|
|
@@ -687,6 +801,8 @@ function renderWidgetUmbrellaHeader(spec) {
|
|
|
687
801
|
}
|
|
688
802
|
function renderWidgetHeader(spec, cppTypes) {
|
|
689
803
|
const widgetClassName = `${spec.widgetName}Widget`;
|
|
804
|
+
const cppCodecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
805
|
+
const dragDropPayloadHelpers = collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog);
|
|
690
806
|
const callbackAliases = [];
|
|
691
807
|
const publicMethods = [];
|
|
692
808
|
const slotMethods = [];
|
|
@@ -696,6 +812,10 @@ function renderWidgetHeader(spec, cppTypes) {
|
|
|
696
812
|
const properties = [];
|
|
697
813
|
const fields = [];
|
|
698
814
|
const publicSlots = [];
|
|
815
|
+
const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) => [
|
|
816
|
+
`static QByteArray encodeDragDropPayload_${helper.typeName}(const ${helper.cppType}& payload);`,
|
|
817
|
+
`static std::optional<${helper.cppType}> decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload);`
|
|
818
|
+
]);
|
|
699
819
|
const bindings = [];
|
|
700
820
|
for (const service of spec.services) {
|
|
701
821
|
for (const member of service.members) {
|
|
@@ -747,6 +867,7 @@ function renderWidgetHeader(spec, cppTypes) {
|
|
|
747
867
|
}
|
|
748
868
|
}
|
|
749
869
|
return `#pragma once
|
|
870
|
+
#include <QByteArray>
|
|
750
871
|
#include <QDateTime>
|
|
751
872
|
#include <QHash>
|
|
752
873
|
#include <QMetaMethod>
|
|
@@ -754,6 +875,7 @@ function renderWidgetHeader(spec, cppTypes) {
|
|
|
754
875
|
#include <QVariant>
|
|
755
876
|
#include <QVariantList>
|
|
756
877
|
#include <functional>
|
|
878
|
+
#include <optional>
|
|
757
879
|
#include "AnQstWebHostBase.h"
|
|
758
880
|
#include "${spec.widgetName}Types.h"
|
|
759
881
|
|
|
@@ -784,6 +906,7 @@ ${handleMethods.join("\n")}
|
|
|
784
906
|
static constexpr const char* kBootstrapContentRoot = "qrc:/${spec.widgetName.toLowerCase()}";
|
|
785
907
|
static constexpr const char* kBootstrapBridgeObject = "${spec.widgetName}Bridge";
|
|
786
908
|
static constexpr int kMaxQueuedCallsPerEndpoint = 1024;
|
|
909
|
+
${dragDropHelperMethods.map((s) => ` ${s}`).join("\n")}
|
|
787
910
|
|
|
788
911
|
handle handle;
|
|
789
912
|
${publicMethods.map((s) => ` ${s}`).join("\n")}
|
|
@@ -836,13 +959,22 @@ ${fields.map((f) => ` ${f}`).join("\n")}
|
|
|
836
959
|
}
|
|
837
960
|
function renderCppStub(spec, cppTypes) {
|
|
838
961
|
const widgetClassName = `${spec.widgetName}Widget`;
|
|
962
|
+
const cppCodecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
963
|
+
const dragDropPayloadHelpers = collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog);
|
|
964
|
+
const cppCodecHelpers = (0, structured_top_level_codecs_1.renderCppStructuredCodecHelpers)(cppCodecCatalog, (typeText, pathHintParts) => cppTypes.mapTypeText(typeText, pathHintParts)).trim();
|
|
839
965
|
const lines = [];
|
|
840
966
|
lines.push(`#include "include/${spec.widgetName}Widget.h"`);
|
|
841
967
|
lines.push(`#include <QDebug>`);
|
|
842
968
|
lines.push(`#include <QElapsedTimer>`);
|
|
843
969
|
lines.push(`#include <QEventLoop>`);
|
|
970
|
+
lines.push(`#include <QJsonArray>`);
|
|
971
|
+
lines.push(`#include <QJsonDocument>`);
|
|
844
972
|
lines.push(`#include <QMetaType>`);
|
|
845
973
|
lines.push(`#include <QTimer>`);
|
|
974
|
+
lines.push(`#include <cstring>`);
|
|
975
|
+
lines.push(`#include <cstdint>`);
|
|
976
|
+
lines.push(`#include <string>`);
|
|
977
|
+
lines.push(`#include <vector>`);
|
|
846
978
|
lines.push(`#include <stdexcept>`);
|
|
847
979
|
lines.push("");
|
|
848
980
|
lines.push(`using namespace ${spec.widgetName};`);
|
|
@@ -860,8 +992,31 @@ function renderCppStub(spec, cppTypes) {
|
|
|
860
992
|
lines.push(" }();");
|
|
861
993
|
lines.push(" Q_UNUSED(registered);");
|
|
862
994
|
lines.push("}");
|
|
995
|
+
if (cppCodecHelpers.length > 0) {
|
|
996
|
+
lines.push("");
|
|
997
|
+
lines.push(cppCodecHelpers);
|
|
998
|
+
}
|
|
863
999
|
lines.push("}");
|
|
864
1000
|
lines.push("");
|
|
1001
|
+
for (const helper of dragDropPayloadHelpers) {
|
|
1002
|
+
lines.push(`QByteArray ${widgetClassName}::encodeDragDropPayload_${helper.typeName}(const ${helper.cppType}& payload) {`);
|
|
1003
|
+
lines.push(` return QJsonDocument::fromVariant(anqstNormalizeWireItems(encode${helper.codecId}(payload))).toJson(QJsonDocument::Compact);`);
|
|
1004
|
+
lines.push(`}`);
|
|
1005
|
+
lines.push("");
|
|
1006
|
+
lines.push(`std::optional<${helper.cppType}> ${widgetClassName}::decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload) {`);
|
|
1007
|
+
lines.push(` QJsonParseError parseError;`);
|
|
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 (...) {`);
|
|
1015
|
+
lines.push(` return std::nullopt;`);
|
|
1016
|
+
lines.push(` }`);
|
|
1017
|
+
lines.push(`}`);
|
|
1018
|
+
lines.push("");
|
|
1019
|
+
}
|
|
865
1020
|
for (const service of spec.services) {
|
|
866
1021
|
for (const member of service.members) {
|
|
867
1022
|
if (member.kind !== "Call" || !member.payloadTypeText)
|
|
@@ -912,17 +1067,19 @@ function renderCppStub(spec, cppTypes) {
|
|
|
912
1067
|
for (const member of service.members) {
|
|
913
1068
|
if (member.kind === "DropTarget" && member.payloadTypeText) {
|
|
914
1069
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1070
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
915
1071
|
lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_dropReceived, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
|
|
916
1072
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
917
|
-
lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
|
|
1073
|
+
lines.push(` emit ${member.name}(${payloadSite ? `decode${payloadSite.codecId}(payload)` : `payload.value<${cppType}>()`}, x, y);`);
|
|
918
1074
|
lines.push(` }`);
|
|
919
1075
|
lines.push(` });`);
|
|
920
1076
|
}
|
|
921
1077
|
else if (member.kind === "HoverTarget" && member.payloadTypeText) {
|
|
922
1078
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1079
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
923
1080
|
lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverUpdated, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
|
|
924
1081
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
925
|
-
lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
|
|
1082
|
+
lines.push(` emit ${member.name}(${payloadSite ? `decode${payloadSite.codecId}(payload)` : `payload.value<${cppType}>()`}, x, y);`);
|
|
926
1083
|
lines.push(` }`);
|
|
927
1084
|
lines.push(` });`);
|
|
928
1085
|
lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverLeft, this, [this](const QString& service, const QString& member) {`);
|
|
@@ -1028,11 +1185,13 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1028
1185
|
continue;
|
|
1029
1186
|
const timeoutMs = member.timeoutMs;
|
|
1030
1187
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1188
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1031
1189
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
1032
1190
|
for (let i = 0; i < member.parameters.length; i++) {
|
|
1033
1191
|
const p = member.parameters[i];
|
|
1034
1192
|
const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
|
|
1035
|
-
|
|
1193
|
+
const paramSite = (0, structured_top_level_codecs_1.getStructuredParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
|
|
1194
|
+
lines.push(` const ${pType} ${p.name} = ${paramSite ? `decode${paramSite.codecId}(args.value(${i}))` : variantToCppExpression(pType, `args.value(${i})`)};`);
|
|
1036
1195
|
}
|
|
1037
1196
|
lines.push(` const QString requestId = QStringLiteral("call-%1").arg(++m_callRequestCounter);`);
|
|
1038
1197
|
lines.push(` const QString queueKey = makeBindingKey(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"));`);
|
|
@@ -1049,7 +1208,7 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1049
1208
|
lines.push(` try {`);
|
|
1050
1209
|
const callArgs = member.parameters.map((p) => p.name).join(", ");
|
|
1051
1210
|
lines.push(` const ${cppType} result = m_${member.name}Handler(${callArgs});`);
|
|
1052
|
-
lines.push(` return ${cppToVariantExpression(cppType, "result")};`);
|
|
1211
|
+
lines.push(` return ${payloadSite ? `encode${payloadSite.codecId}(result)` : cppToVariantExpression(cppType, "result")};`);
|
|
1053
1212
|
lines.push(` } catch (const std::exception& ex) {`);
|
|
1054
1213
|
lines.push(` return QVariantMap{`);
|
|
1055
1214
|
lines.push(` {QStringLiteral("code"), QStringLiteral("CallHandlerError")},`);
|
|
@@ -1101,7 +1260,8 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1101
1260
|
for (let i = 0; i < member.parameters.length; i++) {
|
|
1102
1261
|
const p = member.parameters[i];
|
|
1103
1262
|
const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
|
|
1104
|
-
|
|
1263
|
+
const paramSite = (0, structured_top_level_codecs_1.getStructuredParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
|
|
1264
|
+
lines.push(` const ${pType} ${p.name} = ${paramSite ? `decode${paramSite.codecId}(args.value(${i}))` : variantToCppExpression(pType, `args.value(${i})`)};`);
|
|
1105
1265
|
}
|
|
1106
1266
|
const argNames = member.parameters.map((p) => p.name).join(", ");
|
|
1107
1267
|
lines.push(` emit ${member.name}(${argNames});`);
|
|
@@ -1117,8 +1277,9 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1117
1277
|
if (member.kind !== "Input" || !member.payloadTypeText)
|
|
1118
1278
|
continue;
|
|
1119
1279
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1280
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1120
1281
|
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
1121
|
-
lines.push(` const ${cppType} typedValue = ${variantToCppExpression(cppType, "value")};`);
|
|
1282
|
+
lines.push(` const ${cppType} typedValue = ${payloadSite ? `decode${payloadSite.codecId}(value)` : variantToCppExpression(cppType, "value")};`);
|
|
1122
1283
|
lines.push(` set${pascalCase(member.name)}(typedValue);`);
|
|
1123
1284
|
lines.push(` if (m_${member.name}Handler) m_${member.name}Handler(typedValue);`);
|
|
1124
1285
|
lines.push(` return;`);
|
|
@@ -1138,12 +1299,14 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1138
1299
|
}
|
|
1139
1300
|
if (member.kind === "Slot") {
|
|
1140
1301
|
const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
|
|
1302
|
+
const payloadSite = member.payloadTypeText ? (0, structured_top_level_codecs_1.getStructuredPayloadSite)(cppCodecCatalog, service.name, member.name) : undefined;
|
|
1141
1303
|
const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
|
|
1142
1304
|
lines.push(`${ret} ${widgetClassName}::slot_${member.name}(${args}) {`);
|
|
1143
1305
|
lines.push(` QVariantList invokeArgs;`);
|
|
1144
1306
|
for (const p of member.parameters) {
|
|
1145
1307
|
const pType = mapTsTypeToCpp(p.typeText);
|
|
1146
|
-
|
|
1308
|
+
const paramSite = (0, structured_top_level_codecs_1.getStructuredParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
|
|
1309
|
+
lines.push(` invokeArgs.push_back(${paramSite ? `encode${paramSite.codecId}(${p.name})` : cppToVariantExpression(pType, p.name)});`);
|
|
1147
1310
|
}
|
|
1148
1311
|
lines.push(` QVariant result;`);
|
|
1149
1312
|
lines.push(` QString invokeError;`);
|
|
@@ -1160,13 +1323,14 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1160
1323
|
lines.push(` return;`);
|
|
1161
1324
|
}
|
|
1162
1325
|
else {
|
|
1163
|
-
lines.push(` return ${variantToCppExpression(ret, "result")};`);
|
|
1326
|
+
lines.push(` return ${payloadSite ? `decode${payloadSite.codecId}(result)` : variantToCppExpression(ret, "result")};`);
|
|
1164
1327
|
}
|
|
1165
1328
|
lines.push("}");
|
|
1166
1329
|
lines.push("");
|
|
1167
1330
|
}
|
|
1168
1331
|
else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
|
|
1169
1332
|
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1333
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(cppCodecCatalog, service.name, member.name);
|
|
1170
1334
|
const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
|
|
1171
1335
|
lines.push(`${cppType} ${widgetClassName}::${member.name}() const {`);
|
|
1172
1336
|
lines.push(` return m_${member.name};`);
|
|
@@ -1174,9 +1338,40 @@ function renderCppStub(spec, cppTypes) {
|
|
|
1174
1338
|
lines.push("");
|
|
1175
1339
|
lines.push(`void ${widgetClassName}::set${cap}(const ${cppType}& value) {`);
|
|
1176
1340
|
lines.push(` if (m_${member.name} == value) return;`);
|
|
1341
|
+
if (member.kind === "Output") {
|
|
1342
|
+
lines.push(` QVariant encodedValue;`);
|
|
1343
|
+
lines.push(` try {`);
|
|
1344
|
+
lines.push(` encodedValue = ${payloadSite ? `encode${payloadSite.codecId}(value)` : cppToVariantExpression(cppType, "value")};`);
|
|
1345
|
+
lines.push(` } catch (const std::exception& ex) {`);
|
|
1346
|
+
lines.push(` emitHostError(`);
|
|
1347
|
+
lines.push(` QStringLiteral("SerializationError"),`);
|
|
1348
|
+
lines.push(` QStringLiteral("bridge"),`);
|
|
1349
|
+
lines.push(` QStringLiteral("error"),`);
|
|
1350
|
+
lines.push(` true,`);
|
|
1351
|
+
lines.push(` QStringLiteral("Failed to serialize Output ${service.name}.${member.name}."),`);
|
|
1352
|
+
lines.push(` {`);
|
|
1353
|
+
lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
|
|
1354
|
+
lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
|
|
1355
|
+
lines.push(` {QStringLiteral("detail"), QString::fromUtf8(ex.what())},`);
|
|
1356
|
+
lines.push(` });`);
|
|
1357
|
+
lines.push(` return;`);
|
|
1358
|
+
lines.push(` } catch (...) {`);
|
|
1359
|
+
lines.push(` emitHostError(`);
|
|
1360
|
+
lines.push(` QStringLiteral("SerializationError"),`);
|
|
1361
|
+
lines.push(` QStringLiteral("bridge"),`);
|
|
1362
|
+
lines.push(` QStringLiteral("error"),`);
|
|
1363
|
+
lines.push(` true,`);
|
|
1364
|
+
lines.push(` QStringLiteral("Failed to serialize Output ${service.name}.${member.name}."),`);
|
|
1365
|
+
lines.push(` {`);
|
|
1366
|
+
lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
|
|
1367
|
+
lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
|
|
1368
|
+
lines.push(` });`);
|
|
1369
|
+
lines.push(` return;`);
|
|
1370
|
+
lines.push(` }`);
|
|
1371
|
+
}
|
|
1177
1372
|
lines.push(` m_${member.name} = value;`);
|
|
1178
1373
|
if (member.kind === "Output") {
|
|
1179
|
-
lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"),
|
|
1374
|
+
lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), encodedValue);`);
|
|
1180
1375
|
}
|
|
1181
1376
|
lines.push(` emit ${member.name}Changed(value);`);
|
|
1182
1377
|
lines.push("}");
|
|
@@ -1363,59 +1558,155 @@ function slotHandlerReturnType(tsRet) {
|
|
|
1363
1558
|
}
|
|
1364
1559
|
return `${tsRet} | Promise<${tsRet}> | Error`;
|
|
1365
1560
|
}
|
|
1366
|
-
function renderTsService(spec, serviceName) {
|
|
1561
|
+
function renderTsService(spec, serviceName, codecCatalog) {
|
|
1367
1562
|
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
1368
1563
|
const fieldLines = [];
|
|
1369
1564
|
const methodLines = [];
|
|
1370
1565
|
const setMembers = [];
|
|
1371
1566
|
const onSlotMembers = [];
|
|
1372
1567
|
const constructorBodyLines = [];
|
|
1373
|
-
constructorBodyLines.push(" this._bridge.ready().catch((error) => console.error('AnQst bridge ready() failed', error, (error as { stack?: unknown })?.stack));");
|
|
1374
1568
|
for (const m of members) {
|
|
1375
1569
|
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
1376
|
-
const
|
|
1377
|
-
const
|
|
1570
|
+
const paramSites = m.parameters.map((p) => (0, structured_top_level_codecs_1.getStructuredParameterSite)(codecCatalog, serviceName, m.name, p.name));
|
|
1571
|
+
const encodedValueArray = paramSites.length > 0
|
|
1572
|
+
? `[${m.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
1573
|
+
: "[]";
|
|
1574
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, serviceName, m.name);
|
|
1378
1575
|
if (m.kind === "Call") {
|
|
1379
1576
|
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
1380
|
-
|
|
1577
|
+
if (payloadSite) {
|
|
1578
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { const result = await this._bridge.call<unknown>("${serviceName}", "${m.name}", ${encodedValueArray}); return decode${payloadSite.codecId}(result); }`);
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${encodedValueArray}); }`);
|
|
1582
|
+
}
|
|
1381
1583
|
continue;
|
|
1382
1584
|
}
|
|
1383
1585
|
if (m.kind === "Emitter") {
|
|
1384
|
-
methodLines.push(` ${m.name}(${args}): void {
|
|
1586
|
+
methodLines.push(` ${m.name}(${args}): void {`);
|
|
1587
|
+
methodLines.push(` let encodedArgs: unknown[];`);
|
|
1588
|
+
methodLines.push(` try {`);
|
|
1589
|
+
methodLines.push(` encodedArgs = ${encodedValueArray};`);
|
|
1590
|
+
methodLines.push(` } catch (error) {`);
|
|
1591
|
+
methodLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
1592
|
+
methodLines.push(` code: "SerializationError",`);
|
|
1593
|
+
methodLines.push(` severity: "error",`);
|
|
1594
|
+
methodLines.push(` category: "bridge",`);
|
|
1595
|
+
methodLines.push(` recoverable: true,`);
|
|
1596
|
+
methodLines.push(` message: \`Failed to serialize Emitter ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
1597
|
+
methodLines.push(` service: "${serviceName}",`);
|
|
1598
|
+
methodLines.push(` member: "${m.name}",`);
|
|
1599
|
+
methodLines.push(` context: { interaction: "Emitter" }`);
|
|
1600
|
+
methodLines.push(` });`);
|
|
1601
|
+
methodLines.push(` return;`);
|
|
1602
|
+
methodLines.push(` }`);
|
|
1603
|
+
methodLines.push(` this._bridge.emit("${serviceName}", "${m.name}", encodedArgs);`);
|
|
1604
|
+
methodLines.push(` }`);
|
|
1385
1605
|
continue;
|
|
1386
1606
|
}
|
|
1387
1607
|
if (m.kind === "Slot") {
|
|
1388
1608
|
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
1609
|
+
const decodedArgs = m.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(wireArgs[${index}])` : `wireArgs[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ");
|
|
1389
1610
|
onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
|
|
1390
|
-
onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}",
|
|
1611
|
+
onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", (...wireArgs: unknown[]) => {`);
|
|
1612
|
+
onSlotMembers.push(` const result = handler(${decodedArgs});`);
|
|
1613
|
+
if (payloadSite) {
|
|
1614
|
+
onSlotMembers.push(` if (result instanceof Promise) return result.then((value) => value instanceof Error ? value : encode${payloadSite.codecId}(value));`);
|
|
1615
|
+
onSlotMembers.push(` return result instanceof Error ? result : encode${payloadSite.codecId}(result);`);
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
onSlotMembers.push(" return result;");
|
|
1619
|
+
}
|
|
1620
|
+
onSlotMembers.push(" });");
|
|
1391
1621
|
onSlotMembers.push(" },");
|
|
1392
1622
|
continue;
|
|
1393
1623
|
}
|
|
1394
1624
|
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
1395
1625
|
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
1396
|
-
fieldLines.push(` private readonly _${m.name} = signal<${tsType}>(
|
|
1397
|
-
methodLines.push(` ${m.name}(): ${tsType} { return this._${m.name}(); }`);
|
|
1626
|
+
fieldLines.push(` private readonly _${m.name} = signal<${tsType} | undefined>(undefined);`);
|
|
1627
|
+
methodLines.push(` ${m.name}(): ${tsType} | undefined { return this._${m.name}(); }`);
|
|
1398
1628
|
if (m.kind === "Input") {
|
|
1399
1629
|
setMembers.push(` ${m.name}: (value: ${tsType}): void => {`);
|
|
1630
|
+
setMembers.push(` let encodedValue: unknown;`);
|
|
1631
|
+
setMembers.push(` try {`);
|
|
1632
|
+
setMembers.push(` encodedValue = ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"};`);
|
|
1633
|
+
setMembers.push(` } catch (error) {`);
|
|
1634
|
+
setMembers.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
1635
|
+
setMembers.push(` code: "SerializationError",`);
|
|
1636
|
+
setMembers.push(` severity: "error",`);
|
|
1637
|
+
setMembers.push(` category: "bridge",`);
|
|
1638
|
+
setMembers.push(` recoverable: true,`);
|
|
1639
|
+
setMembers.push(` message: \`Failed to serialize Input ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
1640
|
+
setMembers.push(` service: "${serviceName}",`);
|
|
1641
|
+
setMembers.push(` member: "${m.name}",`);
|
|
1642
|
+
setMembers.push(` context: { interaction: "Input" }`);
|
|
1643
|
+
setMembers.push(` });`);
|
|
1644
|
+
setMembers.push(` return;`);
|
|
1645
|
+
setMembers.push(` }`);
|
|
1400
1646
|
setMembers.push(` this._${m.name}.set(value);`);
|
|
1401
|
-
setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}",
|
|
1647
|
+
setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", encodedValue);`);
|
|
1402
1648
|
setMembers.push(" },");
|
|
1403
1649
|
}
|
|
1404
1650
|
if (m.kind === "Output") {
|
|
1405
|
-
constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) =>
|
|
1651
|
+
constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => {`);
|
|
1652
|
+
constructorBodyLines.push(` try {`);
|
|
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(` }`);
|
|
1666
|
+
constructorBodyLines.push(` });`);
|
|
1406
1667
|
}
|
|
1407
1668
|
}
|
|
1408
1669
|
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
1409
1670
|
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
1410
1671
|
fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
1411
1672
|
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
|
|
1412
|
-
constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) =>
|
|
1673
|
+
constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
1674
|
+
constructorBodyLines.push(` try {`);
|
|
1675
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decode${payloadSite.codecId}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
1676
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
1677
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
1678
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
1679
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
1680
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
1681
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
1682
|
+
constructorBodyLines.push(` message: \`Failed to deserialize DropTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
1683
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
1684
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
1685
|
+
constructorBodyLines.push(` context: { interaction: "DropTarget" }`);
|
|
1686
|
+
constructorBodyLines.push(` });`);
|
|
1687
|
+
constructorBodyLines.push(` }`);
|
|
1688
|
+
constructorBodyLines.push(` });`);
|
|
1413
1689
|
}
|
|
1414
1690
|
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
1415
1691
|
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
1416
1692
|
fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
1417
1693
|
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
|
|
1418
|
-
constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) =>
|
|
1694
|
+
constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
1695
|
+
constructorBodyLines.push(` try {`);
|
|
1696
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decode${payloadSite.codecId}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
1697
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
1698
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
1699
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
1700
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
1701
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
1702
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
1703
|
+
constructorBodyLines.push(` message: \`Failed to deserialize HoverTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
1704
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
1705
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
1706
|
+
constructorBodyLines.push(` context: { interaction: "HoverTarget" }`);
|
|
1707
|
+
constructorBodyLines.push(` });`);
|
|
1708
|
+
constructorBodyLines.push(` }`);
|
|
1709
|
+
constructorBodyLines.push(` });`);
|
|
1419
1710
|
constructorBodyLines.push(` this._bridge.onHoverLeft("${serviceName}", "${m.name}", () => this._${m.name}.set(null));`);
|
|
1420
1711
|
}
|
|
1421
1712
|
}
|
|
@@ -1464,7 +1755,7 @@ function renderTsServiceDts(spec, serviceName) {
|
|
|
1464
1755
|
}
|
|
1465
1756
|
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
1466
1757
|
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
1467
|
-
classMembers.push(` ${m.name}(): ${tsType};`);
|
|
1758
|
+
classMembers.push(` ${m.name}(): ${tsType} | undefined;`);
|
|
1468
1759
|
if (m.kind === "Input") {
|
|
1469
1760
|
setMembers.push(` ${m.name}(value: ${tsType}): void;`);
|
|
1470
1761
|
}
|
|
@@ -1495,7 +1786,8 @@ export declare class ${serviceName} {
|
|
|
1495
1786
|
}`;
|
|
1496
1787
|
}
|
|
1497
1788
|
function renderTsServices(spec) {
|
|
1498
|
-
const
|
|
1789
|
+
const codecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
1790
|
+
const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name, codecCatalog)).join("\n");
|
|
1499
1791
|
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/services.ts`).trim();
|
|
1500
1792
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
1501
1793
|
const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
|
|
@@ -1503,6 +1795,9 @@ function renderTsServices(spec) {
|
|
|
1503
1795
|
return `import { Injectable, inject, signal } from "@angular/core";
|
|
1504
1796
|
${typeImportsBlock}
|
|
1505
1797
|
|
|
1798
|
+
// Structured/top-level codec helpers
|
|
1799
|
+
${(0, structured_top_level_codecs_1.renderTsStructuredCodecHelpers)(codecCatalog)}
|
|
1800
|
+
|
|
1506
1801
|
type SlotHandler = (...args: unknown[]) => unknown;
|
|
1507
1802
|
type OutputHandler = (value: unknown) => void;
|
|
1508
1803
|
type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
|
|
@@ -1510,6 +1805,28 @@ type OutputListener = (service: string, member: string, value: unknown) => void;
|
|
|
1510
1805
|
type DropListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
1511
1806
|
type HoverListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
1512
1807
|
type HoverLeftListener = (service: string, member: string) => void;
|
|
1808
|
+
type HostDiagnosticListener = (payload: unknown) => void;
|
|
1809
|
+
type DisconnectListener = () => void;
|
|
1810
|
+
|
|
1811
|
+
export type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
1812
|
+
export type AnQstBridgeSource = "frontend" | "host";
|
|
1813
|
+
export type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
1814
|
+
export type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
1815
|
+
|
|
1816
|
+
export interface AnQstBridgeDiagnostic {
|
|
1817
|
+
code: string;
|
|
1818
|
+
severity: AnQstBridgeSeverity;
|
|
1819
|
+
category: string;
|
|
1820
|
+
recoverable: boolean;
|
|
1821
|
+
message: string;
|
|
1822
|
+
timestamp: string;
|
|
1823
|
+
source: AnQstBridgeSource;
|
|
1824
|
+
transport?: AnQstBridgeTransport;
|
|
1825
|
+
service?: string;
|
|
1826
|
+
member?: string;
|
|
1827
|
+
requestId?: string;
|
|
1828
|
+
context?: Record<string, unknown>;
|
|
1829
|
+
}
|
|
1513
1830
|
|
|
1514
1831
|
interface HostBridgeApi {
|
|
1515
1832
|
anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
|
|
@@ -1521,6 +1838,7 @@ interface HostBridgeApi {
|
|
|
1521
1838
|
anQstBridge_slotInvocationRequested: {
|
|
1522
1839
|
connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
|
|
1523
1840
|
};
|
|
1841
|
+
anQstBridge_hostDiagnostic?: { connect: (cb: (payload: unknown) => void) => void };
|
|
1524
1842
|
anQstBridge_dropReceived: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
1525
1843
|
anQstBridge_hoverUpdated: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
1526
1844
|
anQstBridge_hoverLeft: { connect: (cb: (service: string, member: string) => void) => void };
|
|
@@ -1534,6 +1852,7 @@ interface QWebChannelCtor {
|
|
|
1534
1852
|
}
|
|
1535
1853
|
|
|
1536
1854
|
interface BridgeAdapter {
|
|
1855
|
+
readonly transport: AnQstBridgeTransport;
|
|
1537
1856
|
call<T>(service: string, member: string, args: unknown[]): Promise<T>;
|
|
1538
1857
|
emit(service: string, member: string, args: unknown[]): void;
|
|
1539
1858
|
setInput(service: string, member: string, value: unknown): void;
|
|
@@ -1541,11 +1860,82 @@ interface BridgeAdapter {
|
|
|
1541
1860
|
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
1542
1861
|
onOutput(handler: OutputListener): void;
|
|
1543
1862
|
onSlotInvocation(handler: SlotInvocationListener): void;
|
|
1863
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void;
|
|
1864
|
+
onDisconnected(handler: DisconnectListener): void;
|
|
1544
1865
|
onDrop(handler: DropListener): void;
|
|
1545
1866
|
onHover(handler: HoverListener): void;
|
|
1546
1867
|
onHoverLeft(handler: HoverLeftListener): void;
|
|
1547
1868
|
}
|
|
1548
1869
|
|
|
1870
|
+
function errorMessage(error: unknown): string {
|
|
1871
|
+
if (error instanceof Error && typeof error.message === "string" && error.message.length > 0) {
|
|
1872
|
+
return error.message;
|
|
1873
|
+
}
|
|
1874
|
+
return String(error);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function normalizeSeverity(value: unknown): AnQstBridgeSeverity {
|
|
1878
|
+
if (value === "info" || value === "warn" || value === "error" || value === "fatal") {
|
|
1879
|
+
return value;
|
|
1880
|
+
}
|
|
1881
|
+
return "error";
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
1885
|
+
if (value === null || typeof value !== "object") {
|
|
1886
|
+
return undefined;
|
|
1887
|
+
}
|
|
1888
|
+
return value as Record<string, unknown>;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function readString(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
1892
|
+
const value = record?.[key];
|
|
1893
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function readBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
|
|
1897
|
+
const value = record?.[key];
|
|
1898
|
+
return typeof value === "boolean" ? value : undefined;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function readContext(record: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
1902
|
+
const context = asRecord(record?.["context"]);
|
|
1903
|
+
return context === undefined ? undefined : context;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function normalizeHostDiagnostic(
|
|
1907
|
+
payload: unknown,
|
|
1908
|
+
transport: AnQstBridgeTransport
|
|
1909
|
+
): Omit<AnQstBridgeDiagnostic, "timestamp"> {
|
|
1910
|
+
const row = asRecord(payload);
|
|
1911
|
+
if (row === undefined) {
|
|
1912
|
+
return {
|
|
1913
|
+
code: "HostDiagnosticMalformed",
|
|
1914
|
+
severity: "error",
|
|
1915
|
+
category: "bridge",
|
|
1916
|
+
recoverable: true,
|
|
1917
|
+
message: "Host emitted a malformed diagnostic payload.",
|
|
1918
|
+
source: "host",
|
|
1919
|
+
transport
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const context = readContext(row);
|
|
1924
|
+
return {
|
|
1925
|
+
code: readString(row, "code") ?? "HostDiagnostic",
|
|
1926
|
+
severity: normalizeSeverity(row["severity"]),
|
|
1927
|
+
category: readString(row, "category") ?? "bridge",
|
|
1928
|
+
recoverable: readBoolean(row, "recoverable") ?? true,
|
|
1929
|
+
message: readString(row, "message") ?? "Host emitted a diagnostic payload.",
|
|
1930
|
+
source: "host",
|
|
1931
|
+
transport,
|
|
1932
|
+
service: readString(row, "service") ?? readString(context, "service"),
|
|
1933
|
+
member: readString(row, "member") ?? readString(context, "member"),
|
|
1934
|
+
requestId: readString(row, "requestId") ?? readString(context, "requestId"),
|
|
1935
|
+
context
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1549
1939
|
function isBridgeCallError(value: unknown): value is {
|
|
1550
1940
|
code: unknown;
|
|
1551
1941
|
message: unknown;
|
|
@@ -1565,6 +1955,8 @@ function isBridgeCallError(value: unknown): value is {
|
|
|
1565
1955
|
}
|
|
1566
1956
|
|
|
1567
1957
|
class QtWebChannelAdapter implements BridgeAdapter {
|
|
1958
|
+
readonly transport = "qt-webchannel" as const;
|
|
1959
|
+
|
|
1568
1960
|
private constructor(private readonly host: HostBridgeApi) {}
|
|
1569
1961
|
|
|
1570
1962
|
static async create(): Promise<QtWebChannelAdapter> {
|
|
@@ -1632,6 +2024,14 @@ class QtWebChannelAdapter implements BridgeAdapter {
|
|
|
1632
2024
|
this.host.anQstBridge_slotInvocationRequested.connect(handler);
|
|
1633
2025
|
}
|
|
1634
2026
|
|
|
2027
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
2028
|
+
this.host.anQstBridge_hostDiagnostic?.connect(handler);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
onDisconnected(_handler: DisconnectListener): void {
|
|
2032
|
+
// QWebChannel does not expose a deterministic disconnect event here.
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1635
2035
|
onDrop(handler: DropListener): void {
|
|
1636
2036
|
this.host.anQstBridge_dropReceived.connect(handler);
|
|
1637
2037
|
}
|
|
@@ -1646,6 +2046,7 @@ class QtWebChannelAdapter implements BridgeAdapter {
|
|
|
1646
2046
|
}
|
|
1647
2047
|
|
|
1648
2048
|
class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
2049
|
+
readonly transport = "dev-websocket" as const;
|
|
1649
2050
|
private readonly pending = new Map<string, {
|
|
1650
2051
|
service: string;
|
|
1651
2052
|
member: string;
|
|
@@ -1655,6 +2056,8 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
|
1655
2056
|
}>();
|
|
1656
2057
|
private readonly outputListeners: OutputListener[] = [];
|
|
1657
2058
|
private readonly slotListeners: SlotInvocationListener[] = [];
|
|
2059
|
+
private readonly hostDiagnosticListeners: HostDiagnosticListener[] = [];
|
|
2060
|
+
private readonly disconnectListeners: DisconnectListener[] = [];
|
|
1658
2061
|
private readonly dropListeners: DropListener[] = [];
|
|
1659
2062
|
private readonly hoverListeners: HoverListener[] = [];
|
|
1660
2063
|
private readonly hoverLeftListeners: HoverLeftListener[] = [];
|
|
@@ -1726,7 +2129,9 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
|
1726
2129
|
return;
|
|
1727
2130
|
}
|
|
1728
2131
|
if (type === "hostError") {
|
|
1729
|
-
|
|
2132
|
+
for (const listener of this.hostDiagnosticListeners) {
|
|
2133
|
+
listener(message["payload"]);
|
|
2134
|
+
}
|
|
1730
2135
|
return;
|
|
1731
2136
|
}
|
|
1732
2137
|
if (type === "widgetReattached") {
|
|
@@ -1745,6 +2150,9 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
|
1745
2150
|
});
|
|
1746
2151
|
}
|
|
1747
2152
|
this.pending.clear();
|
|
2153
|
+
for (const listener of this.disconnectListeners) {
|
|
2154
|
+
listener();
|
|
2155
|
+
}
|
|
1748
2156
|
});
|
|
1749
2157
|
}
|
|
1750
2158
|
|
|
@@ -1813,6 +2221,14 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
|
1813
2221
|
this.slotListeners.push(handler);
|
|
1814
2222
|
}
|
|
1815
2223
|
|
|
2224
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
2225
|
+
this.hostDiagnosticListeners.push(handler);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
onDisconnected(handler: DisconnectListener): void {
|
|
2229
|
+
this.disconnectListeners.push(handler);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1816
2232
|
onDrop(handler: DropListener): void {
|
|
1817
2233
|
this.dropListeners.push(handler);
|
|
1818
2234
|
}
|
|
@@ -1828,53 +2244,116 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
|
1828
2244
|
|
|
1829
2245
|
@Injectable({ providedIn: "root" })
|
|
1830
2246
|
class AnQstBridgeRuntime {
|
|
2247
|
+
private static readonly maxDiagnostics = 50;
|
|
1831
2248
|
private adapter: BridgeAdapter | null = null;
|
|
1832
2249
|
private readonly slotHandlers = new Map<string, SlotHandler>();
|
|
1833
2250
|
private readonly outputHandlers = new Map<string, OutputHandler[]>();
|
|
1834
2251
|
private readonly dropHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
1835
2252
|
private readonly hoverHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
1836
2253
|
private readonly hoverLeftHandlers = new Map<string, (() => void)[]>();
|
|
1837
|
-
private readonly
|
|
2254
|
+
private readonly diagnosticListeners = new Set<(diagnostic: AnQstBridgeDiagnostic) => void>();
|
|
2255
|
+
private readonly _diagnostics = signal<readonly AnQstBridgeDiagnostic[]>([]);
|
|
2256
|
+
private readonly _state = signal<AnQstBridgeState>("starting");
|
|
2257
|
+
private readonly startup = this.init().catch((error) => {
|
|
2258
|
+
this._state.set("failed");
|
|
2259
|
+
this.reportFrontendDiagnostic({
|
|
2260
|
+
code: "BridgeBootstrapError",
|
|
2261
|
+
severity: "fatal",
|
|
2262
|
+
category: "bridge",
|
|
2263
|
+
recoverable: false,
|
|
2264
|
+
message: \`Failed to initialize bridge: \${errorMessage(error)}\`
|
|
2265
|
+
});
|
|
2266
|
+
throw error;
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
2270
|
+
return this._diagnostics();
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
state(): AnQstBridgeState {
|
|
2274
|
+
return this._state();
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
subscribeDiagnostics(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
2278
|
+
this.diagnosticListeners.add(listener);
|
|
2279
|
+
return () => this.diagnosticListeners.delete(listener);
|
|
2280
|
+
}
|
|
1838
2281
|
|
|
1839
2282
|
async ready(): Promise<void> {
|
|
1840
2283
|
return this.startup;
|
|
1841
2284
|
}
|
|
1842
2285
|
|
|
2286
|
+
reportFrontendDiagnostic(diagnostic: Omit<AnQstBridgeDiagnostic, "timestamp" | "source">): void {
|
|
2287
|
+
this.pushDiagnostic({
|
|
2288
|
+
...diagnostic,
|
|
2289
|
+
source: "frontend",
|
|
2290
|
+
transport: diagnostic.transport ?? this.adapter?.transport,
|
|
2291
|
+
timestamp: new Date().toISOString()
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
|
|
1843
2295
|
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
1844
2296
|
const adapter = await this.requireAdapter();
|
|
1845
2297
|
return adapter.call<T>(service, member, args);
|
|
1846
2298
|
}
|
|
1847
2299
|
|
|
1848
2300
|
emit(service: string, member: string, args: unknown[]): void {
|
|
1849
|
-
|
|
1850
|
-
this.adapter.emit(service, member, args);
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
this.ready()
|
|
1854
|
-
.then(() => this.requireAdapterSync().emit(service, member, args))
|
|
1855
|
-
.catch((error) => console.error(error));
|
|
2301
|
+
this.publishNonCall("Emitter", service, member, (adapter) => adapter.emit(service, member, args));
|
|
1856
2302
|
}
|
|
1857
2303
|
|
|
1858
2304
|
setInput(service: string, member: string, value: unknown): void {
|
|
1859
|
-
|
|
1860
|
-
this.adapter.setInput(service, member, value);
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
this.ready()
|
|
1864
|
-
.then(() => this.requireAdapterSync().setInput(service, member, value))
|
|
1865
|
-
.catch((error) => console.error(error));
|
|
2305
|
+
this.publishNonCall("Input", service, member, (adapter) => adapter.setInput(service, member, value));
|
|
1866
2306
|
}
|
|
1867
2307
|
|
|
1868
2308
|
registerSlot(service: string, member: string, handler: SlotHandler): void {
|
|
1869
2309
|
const key = this.key(service, member);
|
|
1870
2310
|
this.slotHandlers.set(key, handler);
|
|
1871
2311
|
if (this.adapter !== null) {
|
|
1872
|
-
|
|
2312
|
+
try {
|
|
2313
|
+
this.adapter.registerSlot(service, member);
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
this.reportFrontendDiagnostic({
|
|
2316
|
+
code: "BridgePublishError",
|
|
2317
|
+
severity: "error",
|
|
2318
|
+
category: "bridge",
|
|
2319
|
+
recoverable: true,
|
|
2320
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
2321
|
+
service,
|
|
2322
|
+
member,
|
|
2323
|
+
context: { interaction: "Slot" }
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
1873
2326
|
return;
|
|
1874
2327
|
}
|
|
1875
2328
|
this.ready()
|
|
1876
|
-
.then(() =>
|
|
1877
|
-
|
|
2329
|
+
.then(() => {
|
|
2330
|
+
try {
|
|
2331
|
+
this.requireAdapterSync().registerSlot(service, member);
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
this.reportFrontendDiagnostic({
|
|
2334
|
+
code: "BridgePublishError",
|
|
2335
|
+
severity: "error",
|
|
2336
|
+
category: "bridge",
|
|
2337
|
+
recoverable: true,
|
|
2338
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
2339
|
+
service,
|
|
2340
|
+
member,
|
|
2341
|
+
context: { interaction: "Slot" }
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
})
|
|
2345
|
+
.catch((error) => {
|
|
2346
|
+
this.reportFrontendDiagnostic({
|
|
2347
|
+
code: "BridgePublishError",
|
|
2348
|
+
severity: "error",
|
|
2349
|
+
category: "bridge",
|
|
2350
|
+
recoverable: true,
|
|
2351
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
2352
|
+
service,
|
|
2353
|
+
member,
|
|
2354
|
+
context: { interaction: "Slot" }
|
|
2355
|
+
});
|
|
2356
|
+
});
|
|
1878
2357
|
}
|
|
1879
2358
|
|
|
1880
2359
|
onOutput(service: string, member: string, handler: OutputHandler): void {
|
|
@@ -1917,6 +2396,73 @@ class AnQstBridgeRuntime {
|
|
|
1917
2396
|
return this.requireAdapterSync();
|
|
1918
2397
|
}
|
|
1919
2398
|
|
|
2399
|
+
private pushDiagnostic(diagnostic: AnQstBridgeDiagnostic): void {
|
|
2400
|
+
const previous = this._diagnostics();
|
|
2401
|
+
const trimmed = previous.length >= AnQstBridgeRuntime.maxDiagnostics
|
|
2402
|
+
? previous.slice(previous.length - (AnQstBridgeRuntime.maxDiagnostics - 1))
|
|
2403
|
+
: previous;
|
|
2404
|
+
const next = [...trimmed, diagnostic];
|
|
2405
|
+
this._diagnostics.set(next);
|
|
2406
|
+
for (const listener of this.diagnosticListeners) {
|
|
2407
|
+
listener(diagnostic);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
private publishNonCall(
|
|
2412
|
+
interaction: "Emitter" | "Input",
|
|
2413
|
+
service: string,
|
|
2414
|
+
member: string,
|
|
2415
|
+
publish: (adapter: BridgeAdapter) => void
|
|
2416
|
+
): void {
|
|
2417
|
+
if (this.adapter !== null) {
|
|
2418
|
+
try {
|
|
2419
|
+
publish(this.adapter);
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
this.reportFrontendDiagnostic({
|
|
2422
|
+
code: "BridgePublishError",
|
|
2423
|
+
severity: "error",
|
|
2424
|
+
category: "bridge",
|
|
2425
|
+
recoverable: true,
|
|
2426
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
2427
|
+
service,
|
|
2428
|
+
member,
|
|
2429
|
+
context: { interaction }
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
this.ready()
|
|
2436
|
+
.then(() => {
|
|
2437
|
+
try {
|
|
2438
|
+
publish(this.requireAdapterSync());
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
this.reportFrontendDiagnostic({
|
|
2441
|
+
code: "BridgePublishError",
|
|
2442
|
+
severity: "error",
|
|
2443
|
+
category: "bridge",
|
|
2444
|
+
recoverable: true,
|
|
2445
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
2446
|
+
service,
|
|
2447
|
+
member,
|
|
2448
|
+
context: { interaction }
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
})
|
|
2452
|
+
.catch((error) => {
|
|
2453
|
+
this.reportFrontendDiagnostic({
|
|
2454
|
+
code: "BridgePublishError",
|
|
2455
|
+
severity: "error",
|
|
2456
|
+
category: "bridge",
|
|
2457
|
+
recoverable: true,
|
|
2458
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
2459
|
+
service,
|
|
2460
|
+
member,
|
|
2461
|
+
context: { interaction }
|
|
2462
|
+
});
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
|
|
1920
2466
|
private async init(): Promise<void> {
|
|
1921
2467
|
const anyWindow = window as unknown as { qt?: { webChannelTransport?: unknown }; QWebChannel?: QWebChannelCtor };
|
|
1922
2468
|
if (typeof anyWindow.QWebChannel === "function" && anyWindow.qt?.webChannelTransport !== undefined) {
|
|
@@ -1925,44 +2471,98 @@ class AnQstBridgeRuntime {
|
|
|
1925
2471
|
this.adapter = await WebSocketBridgeAdapter.create();
|
|
1926
2472
|
}
|
|
1927
2473
|
|
|
1928
|
-
this.adapter
|
|
2474
|
+
const adapter = this.adapter;
|
|
2475
|
+
adapter.onHostDiagnostic((payload) => {
|
|
2476
|
+
this.pushDiagnostic({
|
|
2477
|
+
...normalizeHostDiagnostic(payload, adapter.transport),
|
|
2478
|
+
timestamp: new Date().toISOString()
|
|
2479
|
+
});
|
|
2480
|
+
});
|
|
2481
|
+
adapter.onDisconnected(() => {
|
|
2482
|
+
this._state.set("disconnected");
|
|
2483
|
+
this.reportFrontendDiagnostic({
|
|
2484
|
+
code: "BridgeDisconnectedError",
|
|
2485
|
+
severity: "error",
|
|
2486
|
+
category: "bridge",
|
|
2487
|
+
recoverable: true,
|
|
2488
|
+
message: "Bridge disconnected.",
|
|
2489
|
+
transport: adapter.transport
|
|
2490
|
+
});
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2493
|
+
adapter.onOutput((service, member, value) => {
|
|
1929
2494
|
const key = this.key(service, member);
|
|
1930
2495
|
for (const outputHandler of this.outputHandlers.get(key) ?? []) {
|
|
1931
2496
|
outputHandler(value);
|
|
1932
2497
|
}
|
|
1933
2498
|
});
|
|
1934
|
-
|
|
2499
|
+
adapter.onSlotInvocation(async (requestId, service, member, args) => {
|
|
1935
2500
|
const key = this.key(service, member);
|
|
1936
2501
|
const handler = this.slotHandlers.get(key);
|
|
1937
2502
|
if (handler === undefined) {
|
|
1938
|
-
this.
|
|
2503
|
+
this.reportFrontendDiagnostic({
|
|
2504
|
+
code: "HandlerNotRegisteredError",
|
|
2505
|
+
severity: "error",
|
|
2506
|
+
category: "bridge",
|
|
2507
|
+
recoverable: true,
|
|
2508
|
+
message: \`No slot handler registered for \${service}.\${member}.\`,
|
|
2509
|
+
service,
|
|
2510
|
+
member,
|
|
2511
|
+
requestId,
|
|
2512
|
+
context: { interaction: "Slot" }
|
|
2513
|
+
});
|
|
2514
|
+
adapter.resolveSlot(requestId, false, undefined, "No slot handler registered.");
|
|
1939
2515
|
return;
|
|
1940
2516
|
}
|
|
1941
2517
|
try {
|
|
1942
2518
|
const result = await Promise.resolve(handler(...args));
|
|
1943
2519
|
if (result instanceof Error) {
|
|
1944
|
-
this.
|
|
2520
|
+
this.reportFrontendDiagnostic({
|
|
2521
|
+
code: "SlotRequestFailed",
|
|
2522
|
+
severity: "error",
|
|
2523
|
+
category: "bridge",
|
|
2524
|
+
recoverable: true,
|
|
2525
|
+
message: result.message.length > 0
|
|
2526
|
+
? result.message
|
|
2527
|
+
: \`Slot \${service}.\${member} returned an Error.\`,
|
|
2528
|
+
service,
|
|
2529
|
+
member,
|
|
2530
|
+
requestId,
|
|
2531
|
+
context: { interaction: "Slot" }
|
|
2532
|
+
});
|
|
2533
|
+
adapter.resolveSlot(requestId, false, undefined, result.message);
|
|
1945
2534
|
return;
|
|
1946
2535
|
}
|
|
1947
|
-
|
|
2536
|
+
adapter.resolveSlot(requestId, true, result, "");
|
|
1948
2537
|
} catch (error) {
|
|
1949
|
-
const message =
|
|
1950
|
-
this.
|
|
2538
|
+
const message = errorMessage(error);
|
|
2539
|
+
this.reportFrontendDiagnostic({
|
|
2540
|
+
code: "SlotHandlerError",
|
|
2541
|
+
severity: "error",
|
|
2542
|
+
category: "bridge",
|
|
2543
|
+
recoverable: true,
|
|
2544
|
+
message: \`Slot handler \${service}.\${member} threw: \${message}\`,
|
|
2545
|
+
service,
|
|
2546
|
+
member,
|
|
2547
|
+
requestId,
|
|
2548
|
+
context: { interaction: "Slot" }
|
|
2549
|
+
});
|
|
2550
|
+
adapter.resolveSlot(requestId, false, undefined, message);
|
|
1951
2551
|
}
|
|
1952
2552
|
});
|
|
1953
|
-
|
|
2553
|
+
adapter.onDrop((service, member, payload, x, y) => {
|
|
1954
2554
|
const key = this.key(service, member);
|
|
1955
2555
|
for (const handler of this.dropHandlers.get(key) ?? []) {
|
|
1956
2556
|
handler(payload, x, y);
|
|
1957
2557
|
}
|
|
1958
2558
|
});
|
|
1959
|
-
|
|
2559
|
+
adapter.onHover((service, member, payload, x, y) => {
|
|
1960
2560
|
const key = this.key(service, member);
|
|
1961
2561
|
for (const handler of this.hoverHandlers.get(key) ?? []) {
|
|
1962
2562
|
handler(payload, x, y);
|
|
1963
2563
|
}
|
|
1964
2564
|
});
|
|
1965
|
-
|
|
2565
|
+
adapter.onHoverLeft((service, member) => {
|
|
1966
2566
|
const key = this.key(service, member);
|
|
1967
2567
|
for (const handler of this.hoverLeftHandlers.get(key) ?? []) {
|
|
1968
2568
|
handler();
|
|
@@ -1971,9 +2571,10 @@ class AnQstBridgeRuntime {
|
|
|
1971
2571
|
for (const key of this.slotHandlers.keys()) {
|
|
1972
2572
|
const parts = key.split("::");
|
|
1973
2573
|
if (parts.length === 2) {
|
|
1974
|
-
|
|
2574
|
+
adapter.registerSlot(parts[0], parts[1]);
|
|
1975
2575
|
}
|
|
1976
2576
|
}
|
|
2577
|
+
this._state.set("ready");
|
|
1977
2578
|
}
|
|
1978
2579
|
|
|
1979
2580
|
private key(service: string, member: string): string {
|
|
@@ -1981,6 +2582,23 @@ class AnQstBridgeRuntime {
|
|
|
1981
2582
|
}
|
|
1982
2583
|
|
|
1983
2584
|
}
|
|
2585
|
+
|
|
2586
|
+
@Injectable({ providedIn: "root" })
|
|
2587
|
+
export class AnQstBridgeDiagnostics {
|
|
2588
|
+
private readonly _bridge = inject(AnQstBridgeRuntime);
|
|
2589
|
+
|
|
2590
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
2591
|
+
return this._bridge.diagnostics();
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
state(): AnQstBridgeState {
|
|
2595
|
+
return this._bridge.state();
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
2599
|
+
return this._bridge.subscribeDiagnostics(listener);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
1984
2602
|
${serviceClasses}
|
|
1985
2603
|
`;
|
|
1986
2604
|
}
|
|
@@ -1993,10 +2611,38 @@ function renderTsTypes(spec) {
|
|
|
1993
2611
|
function renderTypeServicesDts(spec) {
|
|
1994
2612
|
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/services.d.ts`).trim();
|
|
1995
2613
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
2614
|
+
const bridgeDiagnosticsDecl = `export type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
2615
|
+
|
|
2616
|
+
export type AnQstBridgeSource = "frontend" | "host";
|
|
2617
|
+
|
|
2618
|
+
export type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
2619
|
+
|
|
2620
|
+
export type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
2621
|
+
|
|
2622
|
+
export interface AnQstBridgeDiagnostic {
|
|
2623
|
+
code: string;
|
|
2624
|
+
severity: AnQstBridgeSeverity;
|
|
2625
|
+
category: string;
|
|
2626
|
+
recoverable: boolean;
|
|
2627
|
+
message: string;
|
|
2628
|
+
timestamp: string;
|
|
2629
|
+
source: AnQstBridgeSource;
|
|
2630
|
+
transport?: AnQstBridgeTransport;
|
|
2631
|
+
service?: string;
|
|
2632
|
+
member?: string;
|
|
2633
|
+
requestId?: string;
|
|
2634
|
+
context?: Record<string, unknown>;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
export declare class AnQstBridgeDiagnostics {
|
|
2638
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[];
|
|
2639
|
+
state(): AnQstBridgeState;
|
|
2640
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void;
|
|
2641
|
+
}`;
|
|
1996
2642
|
const serviceDecls = spec.services
|
|
1997
2643
|
.map((s) => renderTsServiceDts(spec, s.name))
|
|
1998
2644
|
.join("\n\n");
|
|
1999
|
-
const sections = [externalTypeImports, localTypeImports, serviceDecls.trim()].filter((s) => s.length > 0);
|
|
2645
|
+
const sections = [externalTypeImports, localTypeImports, bridgeDiagnosticsDecl, serviceDecls.trim()].filter((s) => s.length > 0);
|
|
2000
2646
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
2001
2647
|
}
|
|
2002
2648
|
function renderTypeTypesDts(spec) {
|
|
@@ -2072,6 +2718,7 @@ function renderNodeExpressWsTypes(spec) {
|
|
|
2072
2718
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
2073
2719
|
}
|
|
2074
2720
|
function renderNodeExpressWsIndex(spec) {
|
|
2721
|
+
const codecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
|
|
2075
2722
|
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
|
|
2076
2723
|
const typeDecls = renderTypeDeclarations(spec, true);
|
|
2077
2724
|
const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
|
|
@@ -2103,8 +2750,13 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2103
2750
|
.map((member) => {
|
|
2104
2751
|
const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
|
|
2105
2752
|
const args = nodeParamArgs(member);
|
|
2753
|
+
const paramSites = member.parameters.map((p) => (0, structured_top_level_codecs_1.getStructuredParameterSite)(codecCatalog, service.name, member.name, p.name));
|
|
2754
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
|
|
2755
|
+
const encodedArgs = member.parameters.length > 0
|
|
2756
|
+
? `[${member.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
2757
|
+
: "[]";
|
|
2106
2758
|
return ` ${service.name}_${member.name}(${args}${args ? ", " : ""}timeoutMs = this.defaultSlotTimeoutMs): Promise<${ret}> {
|
|
2107
|
-
return this.invokeSlot("${service.name}", "${member.name}", ${
|
|
2759
|
+
return this.invokeSlot("${service.name}", "${member.name}", ${encodedArgs}, timeoutMs).then((value) => ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${ret}`});
|
|
2108
2760
|
}`;
|
|
2109
2761
|
}))
|
|
2110
2762
|
.join("\n");
|
|
@@ -2113,8 +2765,9 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2113
2765
|
.filter((member) => member.kind === "Output" && member.payloadTypeText)
|
|
2114
2766
|
.map((member) => {
|
|
2115
2767
|
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
2768
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
|
|
2116
2769
|
return ` set${service.name}_${nodeCap(member.name)}(value: ${typeText}): void {
|
|
2117
|
-
this.setOutputValue("${service.name}", "${member.name}", value);
|
|
2770
|
+
this.setOutputValue("${service.name}", "${member.name}", ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"});
|
|
2118
2771
|
}`;
|
|
2119
2772
|
}))
|
|
2120
2773
|
.join("\n");
|
|
@@ -2167,8 +2820,9 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2167
2820
|
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
2168
2821
|
.map((member) => {
|
|
2169
2822
|
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
2823
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
|
|
2170
2824
|
if (member.kind === "Input") {
|
|
2171
|
-
return ` ${member.name}: {\n get: () => session.readInput("${service.name}", "${member.name}") as
|
|
2825
|
+
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 },`;
|
|
2172
2826
|
}
|
|
2173
2827
|
return ` ${member.name}: {\n set: (value: ${typeText}) => session.set${service.name}_${nodeCap(member.name)}(value)\n },`;
|
|
2174
2828
|
})
|
|
@@ -2180,6 +2834,11 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2180
2834
|
.flatMap((service) => service.members
|
|
2181
2835
|
.filter((member) => member.kind === "Call" && member.payloadTypeText)
|
|
2182
2836
|
.map((member) => {
|
|
2837
|
+
const paramSites = member.parameters.map((p) => (0, structured_top_level_codecs_1.getStructuredParameterSite)(codecCatalog, service.name, member.name, p.name));
|
|
2838
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
|
|
2839
|
+
const decodedArgs = member.parameters.length > 0
|
|
2840
|
+
? member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")
|
|
2841
|
+
: "";
|
|
2183
2842
|
return ` if (service === "${service.name}" && member === "${member.name}") {
|
|
2184
2843
|
const handler = implementation.${service.name}.${member.name};
|
|
2185
2844
|
if (typeof handler !== "function") {
|
|
@@ -2202,8 +2861,8 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2202
2861
|
});
|
|
2203
2862
|
throw err;
|
|
2204
2863
|
}
|
|
2205
|
-
Promise.resolve(handler(buildHandlerBridge(session)
|
|
2206
|
-
.then((result) => sendJson(session.socket, { type: "callResult", requestId, result }))
|
|
2864
|
+
Promise.resolve(handler(buildHandlerBridge(session)${decodedArgs ? `, ${decodedArgs}` : ""}))
|
|
2865
|
+
.then((result) => sendJson(session.socket, { type: "callResult", requestId, result: ${payloadSite ? `encode${payloadSite.codecId}(result)` : "result"} }))
|
|
2207
2866
|
.catch((error) => {
|
|
2208
2867
|
const message = error instanceof Error ? error.message : String(error);
|
|
2209
2868
|
emitDiagnostic({
|
|
@@ -2231,6 +2890,13 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2231
2890
|
.flatMap((service) => service.members
|
|
2232
2891
|
.filter((member) => member.kind === "Emitter")
|
|
2233
2892
|
.map((member) => {
|
|
2893
|
+
const paramSites = member.parameters.map((p) => (0, structured_top_level_codecs_1.getStructuredParameterSite)(codecCatalog, service.name, member.name, p.name));
|
|
2894
|
+
const decodedArgs = member.parameters.length > 0
|
|
2895
|
+
? member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")
|
|
2896
|
+
: "";
|
|
2897
|
+
const decodedSignalArgs = member.parameters.length > 0
|
|
2898
|
+
? `[${member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")}]`
|
|
2899
|
+
: "[]";
|
|
2234
2900
|
return ` if (service === "${service.name}" && member === "${member.name}") {
|
|
2235
2901
|
const handler = implementation.${service.name}.${member.name};
|
|
2236
2902
|
if (typeof handler !== "function") {
|
|
@@ -2247,7 +2913,8 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2247
2913
|
});
|
|
2248
2914
|
throw err;
|
|
2249
2915
|
}
|
|
2250
|
-
|
|
2916
|
+
session.emitSignal(service, member, ${decodedSignalArgs});
|
|
2917
|
+
void Promise.resolve(handler(buildHandlerBridge(session)${decodedArgs ? `, ${decodedArgs}` : ""})).catch((error) => {
|
|
2251
2918
|
const message = error instanceof Error ? error.message : String(error);
|
|
2252
2919
|
emitDiagnostic({
|
|
2253
2920
|
code: "EmitterHandlerError",
|
|
@@ -2268,6 +2935,7 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2268
2935
|
.flatMap((service) => service.members
|
|
2269
2936
|
.filter((member) => member.kind === "Input" && member.payloadTypeText)
|
|
2270
2937
|
.map((member) => {
|
|
2938
|
+
const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
|
|
2271
2939
|
return ` if (service === "${service.name}" && member === "${member.name}") {
|
|
2272
2940
|
const handler = implementation.${service.name}.${member.name};
|
|
2273
2941
|
if (typeof handler !== "function") {
|
|
@@ -2284,7 +2952,9 @@ function renderNodeExpressWsIndex(spec) {
|
|
|
2284
2952
|
});
|
|
2285
2953
|
throw err;
|
|
2286
2954
|
}
|
|
2287
|
-
|
|
2955
|
+
const decodedValue = ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${mapTypeTextToTs(member.payloadTypeText)}`};
|
|
2956
|
+
session.setInputState(service, member, decodedValue);
|
|
2957
|
+
void Promise.resolve(handler(buildHandlerBridge(session), decodedValue)).catch((error) => {
|
|
2288
2958
|
const message = error instanceof Error ? error.message : String(error);
|
|
2289
2959
|
emitDiagnostic({
|
|
2290
2960
|
code: "InputHandlerError",
|
|
@@ -2306,6 +2976,9 @@ import type { WebSocket, WebSocketServer } from "ws";
|
|
|
2306
2976
|
${typeImports}
|
|
2307
2977
|
${typeDecls}
|
|
2308
2978
|
|
|
2979
|
+
// Structured/top-level codec helpers
|
|
2980
|
+
${(0, structured_top_level_codecs_1.renderTsStructuredCodecHelpers)(codecCatalog)}
|
|
2981
|
+
|
|
2309
2982
|
${handlerInterfaces}
|
|
2310
2983
|
|
|
2311
2984
|
export interface ${spec.widgetName}NodeImplementation {
|
|
@@ -2672,7 +3345,6 @@ ${callDispatch}
|
|
|
2672
3345
|
const service = String(message.service ?? "");
|
|
2673
3346
|
const member = String(message.member ?? "");
|
|
2674
3347
|
const args = Array.isArray(message.args) ? (message.args as unknown[]) : [];
|
|
2675
|
-
session.emitSignal(service, member, args);
|
|
2676
3348
|
${emitterDispatch}
|
|
2677
3349
|
const err = new Error(\`No Emitter mapping found for \${service}.\${member}\`);
|
|
2678
3350
|
emitDiagnostic({
|
|
@@ -2691,7 +3363,6 @@ ${emitterDispatch}
|
|
|
2691
3363
|
const service = String(message.service ?? "");
|
|
2692
3364
|
const member = String(message.member ?? "");
|
|
2693
3365
|
const value = message.value;
|
|
2694
|
-
session.setInputState(service, member, value);
|
|
2695
3366
|
${inputDispatch}
|
|
2696
3367
|
const err = new Error(\`No Input mapping found for \${service}.\${member}\`);
|
|
2697
3368
|
emitDiagnostic({
|
|
@@ -2985,13 +3656,15 @@ function renderQtIntegrationCMake(widgetName) {
|
|
|
2985
3656
|
const generatedRootVar = "ANQST_GENERATED_WIDGET_DIR";
|
|
2986
3657
|
const generatedIncludeVar = "ANQST_GENERATED_INCLUDE_DIR";
|
|
2987
3658
|
const projectRootVar = "ANQST_PROJECT_ROOT";
|
|
3659
|
+
const requiredFilesVar = "ANQST_REQUIRED_GENERATED_FILES";
|
|
3660
|
+
const widgetBinaryDirVar = "ANQST_GENERATED_WIDGET_BINARY_DIR";
|
|
2988
3661
|
const widgetTarget = `${widgetName}Widget`;
|
|
2989
|
-
const autogenTarget = `${widgetTarget}_anqst_codegen`;
|
|
2990
3662
|
return `cmake_minimum_required(VERSION 3.21)
|
|
2991
3663
|
|
|
2992
3664
|
set(${projectRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../../../../..")
|
|
2993
3665
|
set(${generatedRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../qt/${generatedCppLibraryDirName(widgetName)}")
|
|
2994
3666
|
set(${generatedIncludeVar} "\${${generatedRootVar}}/include")
|
|
3667
|
+
set(${widgetBinaryDirVar} "\${CMAKE_CURRENT_BINARY_DIR}/${generatedCppLibraryDirName(widgetName)}")
|
|
2995
3668
|
|
|
2996
3669
|
if(TARGET ${widgetTarget})
|
|
2997
3670
|
return()
|
|
@@ -3001,66 +3674,27 @@ if(NOT TARGET anqstwebhostbase)
|
|
|
3001
3674
|
message(FATAL_ERROR "Target 'anqstwebhostbase' must exist before including generated AnQst CMake for ${widgetName}.")
|
|
3002
3675
|
endif()
|
|
3003
3676
|
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
set(CMAKE_AUTOUIC ON)
|
|
3007
|
-
set(CMAKE_AUTORCC ON)
|
|
3008
|
-
|
|
3009
|
-
find_program(ANQST_NPM_EXECUTABLE npm REQUIRED)
|
|
3010
|
-
find_program(ANQST_NPX_EXECUTABLE npx REQUIRED)
|
|
3011
|
-
|
|
3012
|
-
add_custom_command(
|
|
3013
|
-
OUTPUT
|
|
3014
|
-
"\${${generatedRootVar}}/CMakeLists.txt"
|
|
3015
|
-
"\${${generatedRootVar}}/${widgetName}.qrc"
|
|
3016
|
-
"\${${generatedRootVar}}/${widgetName}.cpp"
|
|
3017
|
-
"\${${generatedIncludeVar}}/${widgetName}.h"
|
|
3018
|
-
"\${${generatedIncludeVar}}/${widgetName}Widget.h"
|
|
3019
|
-
"\${${generatedIncludeVar}}/${widgetName}Types.h"
|
|
3020
|
-
"\${${generatedRootVar}}/webapp/index.html"
|
|
3021
|
-
COMMAND "\${ANQST_NPM_EXECUTABLE}" install
|
|
3022
|
-
COMMAND "\${ANQST_NPX_EXECUTABLE}" anqst build
|
|
3023
|
-
WORKING_DIRECTORY "\${${projectRootVar}}"
|
|
3024
|
-
COMMENT "Generating AnQst widget library (${widgetTarget}) from Angular project"
|
|
3025
|
-
VERBATIM
|
|
3026
|
-
)
|
|
3027
|
-
|
|
3028
|
-
add_custom_target(${autogenTarget}
|
|
3029
|
-
DEPENDS
|
|
3030
|
-
"\${${generatedRootVar}}/CMakeLists.txt"
|
|
3031
|
-
"\${${generatedRootVar}}/${widgetName}.qrc"
|
|
3032
|
-
"\${${generatedRootVar}}/${widgetName}.cpp"
|
|
3033
|
-
"\${${generatedIncludeVar}}/${widgetName}.h"
|
|
3034
|
-
"\${${generatedIncludeVar}}/${widgetName}Widget.h"
|
|
3035
|
-
"\${${generatedIncludeVar}}/${widgetName}Types.h"
|
|
3036
|
-
"\${${generatedRootVar}}/webapp/index.html"
|
|
3037
|
-
)
|
|
3038
|
-
|
|
3039
|
-
set_source_files_properties(
|
|
3677
|
+
set(${requiredFilesVar}
|
|
3678
|
+
"\${${generatedRootVar}}/CMakeLists.txt"
|
|
3040
3679
|
"\${${generatedRootVar}}/${widgetName}.qrc"
|
|
3041
3680
|
"\${${generatedRootVar}}/${widgetName}.cpp"
|
|
3042
3681
|
"\${${generatedIncludeVar}}/${widgetName}.h"
|
|
3043
3682
|
"\${${generatedIncludeVar}}/${widgetName}Widget.h"
|
|
3044
3683
|
"\${${generatedIncludeVar}}/${widgetName}Types.h"
|
|
3045
|
-
|
|
3684
|
+
"\${${generatedRootVar}}/webapp/index.html"
|
|
3046
3685
|
)
|
|
3047
3686
|
|
|
3048
|
-
|
|
3049
|
-
"\${
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
)
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
)
|
|
3060
|
-
target_link_libraries(${widgetTarget}
|
|
3061
|
-
PUBLIC
|
|
3062
|
-
anqstwebhostbase
|
|
3063
|
-
)
|
|
3687
|
+
foreach(required_file IN LISTS ${requiredFilesVar})
|
|
3688
|
+
if(NOT EXISTS "\${required_file}")
|
|
3689
|
+
message(FATAL_ERROR
|
|
3690
|
+
"Generated AnQst widget tree is incomplete for ${widgetName}. "
|
|
3691
|
+
"Missing file: \${required_file}. "
|
|
3692
|
+
"Run 'npx anqst build' in '\${${projectRootVar}}' first."
|
|
3693
|
+
)
|
|
3694
|
+
endif()
|
|
3695
|
+
endforeach()
|
|
3696
|
+
|
|
3697
|
+
add_subdirectory("\${${generatedRootVar}}" "\${${widgetBinaryDirVar}}")
|
|
3064
3698
|
`;
|
|
3065
3699
|
}
|
|
3066
3700
|
function installQtIntegrationCMake(cwd, widgetName) {
|