@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.
Files changed (75) hide show
  1. package/README.md +15 -0
  2. package/dist/src/app.js +1 -0
  3. package/dist/src/base93.js +124 -0
  4. package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/decoder.js +35 -0
  5. package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/encoder.js +36 -0
  6. package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/decoder.js +26 -0
  7. package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/encoder.js +38 -0
  8. package/dist/src/codecgenerators/basecodecemitters/binary-blob/decoder.js +28 -0
  9. package/dist/src/codecgenerators/basecodecemitters/binary-blob/encoder.js +34 -0
  10. package/dist/src/codecgenerators/basecodecemitters/binary-buffer/decoder.js +29 -0
  11. package/dist/src/codecgenerators/basecodecemitters/binary-buffer/encoder.js +36 -0
  12. package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/decoder.js +46 -0
  13. package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/encoder.js +49 -0
  14. package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/decoder.js +46 -0
  15. package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/encoder.js +47 -0
  16. package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/decoder.js +46 -0
  17. package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/encoder.js +49 -0
  18. package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/decoder.js +50 -0
  19. package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/encoder.js +52 -0
  20. package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/decoder.js +38 -0
  21. package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/encoder.js +44 -0
  22. package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/decoder.js +33 -0
  23. package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/encoder.js +34 -0
  24. package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/decoder.js +46 -0
  25. package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/encoder.js +49 -0
  26. package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/decoder.js +46 -0
  27. package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/encoder.js +49 -0
  28. package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/decoder.js +28 -0
  29. package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/encoder.js +34 -0
  30. package/dist/src/codecgenerators/basecodecemitters/boolean/decoder.js +34 -0
  31. package/dist/src/codecgenerators/basecodecemitters/boolean/encoder.js +40 -0
  32. package/dist/src/codecgenerators/basecodecemitters/dynamic-json/decoder.js +43 -0
  33. package/dist/src/codecgenerators/basecodecemitters/dynamic-json/encoder.js +45 -0
  34. package/dist/src/codecgenerators/basecodecemitters/dynamic-object/decoder.js +44 -0
  35. package/dist/src/codecgenerators/basecodecemitters/dynamic-object/encoder.js +46 -0
  36. package/dist/src/codecgenerators/basecodecemitters/integer-int16/decoder.js +32 -0
  37. package/dist/src/codecgenerators/basecodecemitters/integer-int16/encoder.js +43 -0
  38. package/dist/src/codecgenerators/basecodecemitters/integer-int32/decoder.js +26 -0
  39. package/dist/src/codecgenerators/basecodecemitters/integer-int32/encoder.js +37 -0
  40. package/dist/src/codecgenerators/basecodecemitters/integer-int8/decoder.js +26 -0
  41. package/dist/src/codecgenerators/basecodecemitters/integer-int8/encoder.js +37 -0
  42. package/dist/src/codecgenerators/basecodecemitters/integer-qint16/decoder.js +36 -0
  43. package/dist/src/codecgenerators/basecodecemitters/integer-qint16/encoder.js +36 -0
  44. package/dist/src/codecgenerators/basecodecemitters/integer-qint32/decoder.js +25 -0
  45. package/dist/src/codecgenerators/basecodecemitters/integer-qint32/encoder.js +36 -0
  46. package/dist/src/codecgenerators/basecodecemitters/integer-qint8/decoder.js +36 -0
  47. package/dist/src/codecgenerators/basecodecemitters/integer-qint8/encoder.js +36 -0
  48. package/dist/src/codecgenerators/basecodecemitters/integer-quint16/decoder.js +26 -0
  49. package/dist/src/codecgenerators/basecodecemitters/integer-quint16/encoder.js +38 -0
  50. package/dist/src/codecgenerators/basecodecemitters/integer-quint32/decoder.js +27 -0
  51. package/dist/src/codecgenerators/basecodecemitters/integer-quint32/encoder.js +39 -0
  52. package/dist/src/codecgenerators/basecodecemitters/integer-quint8/decoder.js +26 -0
  53. package/dist/src/codecgenerators/basecodecemitters/integer-quint8/encoder.js +38 -0
  54. package/dist/src/codecgenerators/basecodecemitters/integer-uint16/decoder.js +30 -0
  55. package/dist/src/codecgenerators/basecodecemitters/integer-uint16/encoder.js +42 -0
  56. package/dist/src/codecgenerators/basecodecemitters/integer-uint32/decoder.js +31 -0
  57. package/dist/src/codecgenerators/basecodecemitters/integer-uint32/encoder.js +43 -0
  58. package/dist/src/codecgenerators/basecodecemitters/integer-uint8/decoder.js +30 -0
  59. package/dist/src/codecgenerators/basecodecemitters/integer-uint8/encoder.js +40 -0
  60. package/dist/src/codecgenerators/basecodecemitters/number/decoder.js +26 -0
  61. package/dist/src/codecgenerators/basecodecemitters/number/encoder.js +38 -0
  62. package/dist/src/codecgenerators/basecodecemitters/shared/comments.js +13 -0
  63. package/dist/src/codecgenerators/basecodecemitters/shared/contracts.js +2 -0
  64. package/dist/src/codecgenerators/basecodecemitters/shared/fixedwidth.js +53 -0
  65. package/dist/src/codecgenerators/basecodecemitters/shared/index.js +21 -0
  66. package/dist/src/codecgenerators/basecodecemitters/shared/positionalBase93.js +48 -0
  67. package/dist/src/codecgenerators/basecodecemitters/shared/rawbytes.js +30 -0
  68. package/dist/src/codecgenerators/basecodecemitters/string/decoder.js +43 -0
  69. package/dist/src/codecgenerators/basecodecemitters/string/encoder.js +43 -0
  70. package/dist/src/codecgenerators/basecodecemitters/stringArray/decoder.js +80 -0
  71. package/dist/src/codecgenerators/basecodecemitters/stringArray/encoder.js +57 -0
  72. package/dist/src/emit.js +752 -118
  73. package/dist/src/structured-top-level-codecs.js +1305 -0
  74. package/package.json +1 -1
  75. 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
- lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
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
- lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
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
- lines.push(` invokeArgs.push_back(${cppToVariantExpression(pType, p.name)});`);
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}"), ${cppToVariantExpression(cppType, "value")});`);
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 valueArgs = m.parameters.map((p) => p.name).join(", ");
1377
- const valueArray = valueArgs.length > 0 ? `[${valueArgs}]` : "[]";
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
- methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${valueArray}); }`);
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 { this._bridge.emit("${serviceName}", "${m.name}", ${valueArray}); }`);
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}", handler as (...args: unknown[]) => unknown);`);
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}>((undefined as unknown) as ${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}", value);`);
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) => this._${m.name}.set(value as ${tsType}));`);
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) => this._${m.name}.set({ payload: payload as ${tsType}, 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) => this._${m.name}.set({ payload: payload as ${tsType}, 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 serviceClasses = spec.services.map((s) => renderTsService(spec, s.name)).join("\n");
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
- console.error("AnQst host error:", message["payload"]);
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 startup = this.init();
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
- if (this.adapter !== null) {
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
- if (this.adapter !== null) {
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
- this.adapter.registerSlot(service, member);
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(() => this.requireAdapterSync().registerSlot(service, member))
1877
- .catch((error) => console.error(error));
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.onOutput((service, member, value) => {
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
- this.adapter.onSlotInvocation(async (requestId, service, member, args) => {
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.adapter!.resolveSlot(requestId, false, undefined, "No slot handler registered.");
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.adapter!.resolveSlot(requestId, false, undefined, result.message);
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
- this.adapter!.resolveSlot(requestId, true, result, "");
2536
+ adapter.resolveSlot(requestId, true, result, "");
1948
2537
  } catch (error) {
1949
- const message = error instanceof Error ? error.message : String(error);
1950
- this.adapter!.resolveSlot(requestId, false, undefined, message);
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
- this.adapter.onDrop((service, member, payload, x, y) => {
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
- this.adapter.onHover((service, member, payload, x, y) => {
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
- this.adapter.onHoverLeft((service, member) => {
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
- this.adapter.registerSlot(parts[0], parts[1]);
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}", ${nodeParamValues(member)}, timeoutMs) as Promise<${ret}>;
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 Promise<${typeText}>,\n on: (handler: (value: ${typeText}) => void) => session.onInput("${service.name}", "${member.name}", handler as (value: unknown) => void)\n },`;
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), ...(args as ${nodeParamTuple(member)})))
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
- void Promise.resolve(handler(buildHandlerBridge(session), ...(args as ${nodeParamTuple(member)}))).catch((error) => {
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
- void Promise.resolve(handler(buildHandlerBridge(session), value as ${mapTypeTextToTs(member.payloadTypeText)})).catch((error) => {
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
- find_package(Qt5 REQUIRED COMPONENTS Core Widgets)
3005
- set(CMAKE_AUTOMOC ON)
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
- PROPERTIES GENERATED TRUE
3684
+ "\${${generatedRootVar}}/webapp/index.html"
3046
3685
  )
3047
3686
 
3048
- add_library(${widgetTarget}
3049
- "\${${generatedRootVar}}/${widgetName}.qrc"
3050
- "\${${generatedRootVar}}/${widgetName}.cpp"
3051
- "\${${generatedIncludeVar}}/${widgetName}.h"
3052
- "\${${generatedIncludeVar}}/${widgetName}Widget.h"
3053
- "\${${generatedIncludeVar}}/${widgetName}Types.h"
3054
- )
3055
- add_dependencies(${widgetTarget} ${autogenTarget})
3056
- target_include_directories(${widgetTarget}
3057
- PUBLIC
3058
- "\${${generatedIncludeVar}}"
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) {