@dusted/anqst 1.0.0 → 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 (76) hide show
  1. package/README.md +15 -0
  2. package/dist/src/app.js +103 -77
  3. package/dist/src/base93.js +124 -0
  4. package/dist/src/build-stamp.js +1 -1
  5. package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/decoder.js +35 -0
  6. package/dist/src/codecgenerators/basecodecemitters/bigint-qint64/encoder.js +36 -0
  7. package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/decoder.js +26 -0
  8. package/dist/src/codecgenerators/basecodecemitters/bigint-quint64/encoder.js +38 -0
  9. package/dist/src/codecgenerators/basecodecemitters/binary-blob/decoder.js +28 -0
  10. package/dist/src/codecgenerators/basecodecemitters/binary-blob/encoder.js +34 -0
  11. package/dist/src/codecgenerators/basecodecemitters/binary-buffer/decoder.js +29 -0
  12. package/dist/src/codecgenerators/basecodecemitters/binary-buffer/encoder.js +36 -0
  13. package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/decoder.js +46 -0
  14. package/dist/src/codecgenerators/basecodecemitters/binary-float32Array/encoder.js +49 -0
  15. package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/decoder.js +46 -0
  16. package/dist/src/codecgenerators/basecodecemitters/binary-float64Array/encoder.js +47 -0
  17. package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/decoder.js +46 -0
  18. package/dist/src/codecgenerators/basecodecemitters/binary-int16Array/encoder.js +49 -0
  19. package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/decoder.js +50 -0
  20. package/dist/src/codecgenerators/basecodecemitters/binary-int32Array/encoder.js +52 -0
  21. package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/decoder.js +38 -0
  22. package/dist/src/codecgenerators/basecodecemitters/binary-int8Array/encoder.js +44 -0
  23. package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/decoder.js +33 -0
  24. package/dist/src/codecgenerators/basecodecemitters/binary-typedArray/encoder.js +34 -0
  25. package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/decoder.js +46 -0
  26. package/dist/src/codecgenerators/basecodecemitters/binary-uint16Array/encoder.js +49 -0
  27. package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/decoder.js +46 -0
  28. package/dist/src/codecgenerators/basecodecemitters/binary-uint32Array/encoder.js +49 -0
  29. package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/decoder.js +28 -0
  30. package/dist/src/codecgenerators/basecodecemitters/binary-uint8Array/encoder.js +34 -0
  31. package/dist/src/codecgenerators/basecodecemitters/boolean/decoder.js +34 -0
  32. package/dist/src/codecgenerators/basecodecemitters/boolean/encoder.js +40 -0
  33. package/dist/src/codecgenerators/basecodecemitters/dynamic-json/decoder.js +43 -0
  34. package/dist/src/codecgenerators/basecodecemitters/dynamic-json/encoder.js +45 -0
  35. package/dist/src/codecgenerators/basecodecemitters/dynamic-object/decoder.js +44 -0
  36. package/dist/src/codecgenerators/basecodecemitters/dynamic-object/encoder.js +46 -0
  37. package/dist/src/codecgenerators/basecodecemitters/integer-int16/decoder.js +32 -0
  38. package/dist/src/codecgenerators/basecodecemitters/integer-int16/encoder.js +43 -0
  39. package/dist/src/codecgenerators/basecodecemitters/integer-int32/decoder.js +26 -0
  40. package/dist/src/codecgenerators/basecodecemitters/integer-int32/encoder.js +37 -0
  41. package/dist/src/codecgenerators/basecodecemitters/integer-int8/decoder.js +26 -0
  42. package/dist/src/codecgenerators/basecodecemitters/integer-int8/encoder.js +37 -0
  43. package/dist/src/codecgenerators/basecodecemitters/integer-qint16/decoder.js +36 -0
  44. package/dist/src/codecgenerators/basecodecemitters/integer-qint16/encoder.js +36 -0
  45. package/dist/src/codecgenerators/basecodecemitters/integer-qint32/decoder.js +25 -0
  46. package/dist/src/codecgenerators/basecodecemitters/integer-qint32/encoder.js +36 -0
  47. package/dist/src/codecgenerators/basecodecemitters/integer-qint8/decoder.js +36 -0
  48. package/dist/src/codecgenerators/basecodecemitters/integer-qint8/encoder.js +36 -0
  49. package/dist/src/codecgenerators/basecodecemitters/integer-quint16/decoder.js +26 -0
  50. package/dist/src/codecgenerators/basecodecemitters/integer-quint16/encoder.js +38 -0
  51. package/dist/src/codecgenerators/basecodecemitters/integer-quint32/decoder.js +27 -0
  52. package/dist/src/codecgenerators/basecodecemitters/integer-quint32/encoder.js +39 -0
  53. package/dist/src/codecgenerators/basecodecemitters/integer-quint8/decoder.js +26 -0
  54. package/dist/src/codecgenerators/basecodecemitters/integer-quint8/encoder.js +38 -0
  55. package/dist/src/codecgenerators/basecodecemitters/integer-uint16/decoder.js +30 -0
  56. package/dist/src/codecgenerators/basecodecemitters/integer-uint16/encoder.js +42 -0
  57. package/dist/src/codecgenerators/basecodecemitters/integer-uint32/decoder.js +31 -0
  58. package/dist/src/codecgenerators/basecodecemitters/integer-uint32/encoder.js +43 -0
  59. package/dist/src/codecgenerators/basecodecemitters/integer-uint8/decoder.js +30 -0
  60. package/dist/src/codecgenerators/basecodecemitters/integer-uint8/encoder.js +40 -0
  61. package/dist/src/codecgenerators/basecodecemitters/number/decoder.js +26 -0
  62. package/dist/src/codecgenerators/basecodecemitters/number/encoder.js +38 -0
  63. package/dist/src/codecgenerators/basecodecemitters/shared/comments.js +13 -0
  64. package/dist/src/codecgenerators/basecodecemitters/shared/contracts.js +2 -0
  65. package/dist/src/codecgenerators/basecodecemitters/shared/fixedwidth.js +53 -0
  66. package/dist/src/codecgenerators/basecodecemitters/shared/index.js +21 -0
  67. package/dist/src/codecgenerators/basecodecemitters/shared/positionalBase93.js +48 -0
  68. package/dist/src/codecgenerators/basecodecemitters/shared/rawbytes.js +30 -0
  69. package/dist/src/codecgenerators/basecodecemitters/string/decoder.js +43 -0
  70. package/dist/src/codecgenerators/basecodecemitters/string/encoder.js +43 -0
  71. package/dist/src/codecgenerators/basecodecemitters/stringArray/decoder.js +80 -0
  72. package/dist/src/codecgenerators/basecodecemitters/stringArray/encoder.js +57 -0
  73. package/dist/src/emit.js +760 -120
  74. package/dist/src/structured-top-level-codecs.js +1305 -0
  75. package/package.json +2 -3
  76. package/spec/AnQst-Spec-DSL.d.ts +15 -8
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")},`);
@@ -1080,7 +1239,13 @@ function renderCppStub(spec, cppTypes) {
1080
1239
  lines.push(` }`);
1081
1240
  }
1082
1241
  }
1083
- lines.push(` return QVariant();`);
1242
+ lines.push(` return QVariantMap{`);
1243
+ lines.push(` {QStringLiteral("code"), QStringLiteral("HandlerNotRegisteredError")},`);
1244
+ lines.push(` {QStringLiteral("message"), QStringLiteral("No Call mapping found.")},`);
1245
+ lines.push(` {QStringLiteral("service"), service},`);
1246
+ lines.push(` {QStringLiteral("member"), member},`);
1247
+ lines.push(` {QStringLiteral("requestId"), QString()}`);
1248
+ lines.push(` };`);
1084
1249
  lines.push(`}`);
1085
1250
  lines.push("");
1086
1251
  lines.push(`void ${widgetClassName}::handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args) {`);
@@ -1095,7 +1260,8 @@ function renderCppStub(spec, cppTypes) {
1095
1260
  for (let i = 0; i < member.parameters.length; i++) {
1096
1261
  const p = member.parameters[i];
1097
1262
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
1098
- 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})`)};`);
1099
1265
  }
1100
1266
  const argNames = member.parameters.map((p) => p.name).join(", ");
1101
1267
  lines.push(` emit ${member.name}(${argNames});`);
@@ -1111,8 +1277,9 @@ function renderCppStub(spec, cppTypes) {
1111
1277
  if (member.kind !== "Input" || !member.payloadTypeText)
1112
1278
  continue;
1113
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);
1114
1281
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
1115
- lines.push(` const ${cppType} typedValue = ${variantToCppExpression(cppType, "value")};`);
1282
+ lines.push(` const ${cppType} typedValue = ${payloadSite ? `decode${payloadSite.codecId}(value)` : variantToCppExpression(cppType, "value")};`);
1116
1283
  lines.push(` set${pascalCase(member.name)}(typedValue);`);
1117
1284
  lines.push(` if (m_${member.name}Handler) m_${member.name}Handler(typedValue);`);
1118
1285
  lines.push(` return;`);
@@ -1132,12 +1299,14 @@ function renderCppStub(spec, cppTypes) {
1132
1299
  }
1133
1300
  if (member.kind === "Slot") {
1134
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;
1135
1303
  const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
1136
1304
  lines.push(`${ret} ${widgetClassName}::slot_${member.name}(${args}) {`);
1137
1305
  lines.push(` QVariantList invokeArgs;`);
1138
1306
  for (const p of member.parameters) {
1139
1307
  const pType = mapTsTypeToCpp(p.typeText);
1140
- 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)});`);
1141
1310
  }
1142
1311
  lines.push(` QVariant result;`);
1143
1312
  lines.push(` QString invokeError;`);
@@ -1154,13 +1323,14 @@ function renderCppStub(spec, cppTypes) {
1154
1323
  lines.push(` return;`);
1155
1324
  }
1156
1325
  else {
1157
- lines.push(` return ${variantToCppExpression(ret, "result")};`);
1326
+ lines.push(` return ${payloadSite ? `decode${payloadSite.codecId}(result)` : variantToCppExpression(ret, "result")};`);
1158
1327
  }
1159
1328
  lines.push("}");
1160
1329
  lines.push("");
1161
1330
  }
1162
1331
  else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
1163
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);
1164
1334
  const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
1165
1335
  lines.push(`${cppType} ${widgetClassName}::${member.name}() const {`);
1166
1336
  lines.push(` return m_${member.name};`);
@@ -1168,9 +1338,40 @@ function renderCppStub(spec, cppTypes) {
1168
1338
  lines.push("");
1169
1339
  lines.push(`void ${widgetClassName}::set${cap}(const ${cppType}& value) {`);
1170
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
+ }
1171
1372
  lines.push(` m_${member.name} = value;`);
1172
1373
  if (member.kind === "Output") {
1173
- lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), ${cppToVariantExpression(cppType, "value")});`);
1374
+ lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), encodedValue);`);
1174
1375
  }
1175
1376
  lines.push(` emit ${member.name}Changed(value);`);
1176
1377
  lines.push("}");
@@ -1357,59 +1558,155 @@ function slotHandlerReturnType(tsRet) {
1357
1558
  }
1358
1559
  return `${tsRet} | Promise<${tsRet}> | Error`;
1359
1560
  }
1360
- function renderTsService(spec, serviceName) {
1561
+ function renderTsService(spec, serviceName, codecCatalog) {
1361
1562
  const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
1362
1563
  const fieldLines = [];
1363
1564
  const methodLines = [];
1364
1565
  const setMembers = [];
1365
1566
  const onSlotMembers = [];
1366
1567
  const constructorBodyLines = [];
1367
- constructorBodyLines.push(" this._bridge.ready().catch((error) => console.error('AnQst bridge ready() failed', error, (error as { stack?: unknown })?.stack));");
1368
1568
  for (const m of members) {
1369
1569
  const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
1370
- const valueArgs = m.parameters.map((p) => p.name).join(", ");
1371
- 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);
1372
1575
  if (m.kind === "Call") {
1373
1576
  const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
1374
- 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
+ }
1375
1583
  continue;
1376
1584
  }
1377
1585
  if (m.kind === "Emitter") {
1378
- 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(` }`);
1379
1605
  continue;
1380
1606
  }
1381
1607
  if (m.kind === "Slot") {
1382
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(", ");
1383
1610
  onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
1384
- 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(" });");
1385
1621
  onSlotMembers.push(" },");
1386
1622
  continue;
1387
1623
  }
1388
1624
  if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
1389
1625
  const tsType = mapTypeTextToTs(m.payloadTypeText);
1390
- fieldLines.push(` private readonly _${m.name} = signal<${tsType}>((undefined as unknown) as ${tsType});`);
1391
- 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}(); }`);
1392
1628
  if (m.kind === "Input") {
1393
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(` }`);
1394
1646
  setMembers.push(` this._${m.name}.set(value);`);
1395
- setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", value);`);
1647
+ setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", encodedValue);`);
1396
1648
  setMembers.push(" },");
1397
1649
  }
1398
1650
  if (m.kind === "Output") {
1399
- 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(` });`);
1400
1667
  }
1401
1668
  }
1402
1669
  if (m.kind === "DropTarget" && m.payloadTypeText) {
1403
1670
  const tsType = mapTypeTextToTs(m.payloadTypeText);
1404
1671
  fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
1405
1672
  methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
1406
- 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(` });`);
1407
1689
  }
1408
1690
  if (m.kind === "HoverTarget" && m.payloadTypeText) {
1409
1691
  const tsType = mapTypeTextToTs(m.payloadTypeText);
1410
1692
  fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
1411
1693
  methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
1412
- 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(` });`);
1413
1710
  constructorBodyLines.push(` this._bridge.onHoverLeft("${serviceName}", "${m.name}", () => this._${m.name}.set(null));`);
1414
1711
  }
1415
1712
  }
@@ -1458,7 +1755,7 @@ function renderTsServiceDts(spec, serviceName) {
1458
1755
  }
1459
1756
  if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
1460
1757
  const tsType = mapTypeTextToTs(m.payloadTypeText);
1461
- classMembers.push(` ${m.name}(): ${tsType};`);
1758
+ classMembers.push(` ${m.name}(): ${tsType} | undefined;`);
1462
1759
  if (m.kind === "Input") {
1463
1760
  setMembers.push(` ${m.name}(value: ${tsType}): void;`);
1464
1761
  }
@@ -1489,7 +1786,8 @@ export declare class ${serviceName} {
1489
1786
  }`;
1490
1787
  }
1491
1788
  function renderTsServices(spec) {
1492
- 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");
1493
1791
  const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/services.ts`).trim();
1494
1792
  const localTypeImports = renderLocalTypeImports(spec).trim();
1495
1793
  const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
@@ -1497,6 +1795,9 @@ function renderTsServices(spec) {
1497
1795
  return `import { Injectable, inject, signal } from "@angular/core";
1498
1796
  ${typeImportsBlock}
1499
1797
 
1798
+ // Structured/top-level codec helpers
1799
+ ${(0, structured_top_level_codecs_1.renderTsStructuredCodecHelpers)(codecCatalog)}
1800
+
1500
1801
  type SlotHandler = (...args: unknown[]) => unknown;
1501
1802
  type OutputHandler = (value: unknown) => void;
1502
1803
  type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
@@ -1504,6 +1805,28 @@ type OutputListener = (service: string, member: string, value: unknown) => void;
1504
1805
  type DropListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
1505
1806
  type HoverListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
1506
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
+ }
1507
1830
 
1508
1831
  interface HostBridgeApi {
1509
1832
  anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
@@ -1515,6 +1838,7 @@ interface HostBridgeApi {
1515
1838
  anQstBridge_slotInvocationRequested: {
1516
1839
  connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
1517
1840
  };
1841
+ anQstBridge_hostDiagnostic?: { connect: (cb: (payload: unknown) => void) => void };
1518
1842
  anQstBridge_dropReceived: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
1519
1843
  anQstBridge_hoverUpdated: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
1520
1844
  anQstBridge_hoverLeft: { connect: (cb: (service: string, member: string) => void) => void };
@@ -1528,6 +1852,7 @@ interface QWebChannelCtor {
1528
1852
  }
1529
1853
 
1530
1854
  interface BridgeAdapter {
1855
+ readonly transport: AnQstBridgeTransport;
1531
1856
  call<T>(service: string, member: string, args: unknown[]): Promise<T>;
1532
1857
  emit(service: string, member: string, args: unknown[]): void;
1533
1858
  setInput(service: string, member: string, value: unknown): void;
@@ -1535,11 +1860,82 @@ interface BridgeAdapter {
1535
1860
  resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
1536
1861
  onOutput(handler: OutputListener): void;
1537
1862
  onSlotInvocation(handler: SlotInvocationListener): void;
1863
+ onHostDiagnostic(handler: HostDiagnosticListener): void;
1864
+ onDisconnected(handler: DisconnectListener): void;
1538
1865
  onDrop(handler: DropListener): void;
1539
1866
  onHover(handler: HoverListener): void;
1540
1867
  onHoverLeft(handler: HoverLeftListener): void;
1541
1868
  }
1542
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
+
1543
1939
  function isBridgeCallError(value: unknown): value is {
1544
1940
  code: unknown;
1545
1941
  message: unknown;
@@ -1559,6 +1955,8 @@ function isBridgeCallError(value: unknown): value is {
1559
1955
  }
1560
1956
 
1561
1957
  class QtWebChannelAdapter implements BridgeAdapter {
1958
+ readonly transport = "qt-webchannel" as const;
1959
+
1562
1960
  private constructor(private readonly host: HostBridgeApi) {}
1563
1961
 
1564
1962
  static async create(): Promise<QtWebChannelAdapter> {
@@ -1626,6 +2024,14 @@ class QtWebChannelAdapter implements BridgeAdapter {
1626
2024
  this.host.anQstBridge_slotInvocationRequested.connect(handler);
1627
2025
  }
1628
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
+
1629
2035
  onDrop(handler: DropListener): void {
1630
2036
  this.host.anQstBridge_dropReceived.connect(handler);
1631
2037
  }
@@ -1640,6 +2046,7 @@ class QtWebChannelAdapter implements BridgeAdapter {
1640
2046
  }
1641
2047
 
1642
2048
  class WebSocketBridgeAdapter implements BridgeAdapter {
2049
+ readonly transport = "dev-websocket" as const;
1643
2050
  private readonly pending = new Map<string, {
1644
2051
  service: string;
1645
2052
  member: string;
@@ -1649,6 +2056,8 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1649
2056
  }>();
1650
2057
  private readonly outputListeners: OutputListener[] = [];
1651
2058
  private readonly slotListeners: SlotInvocationListener[] = [];
2059
+ private readonly hostDiagnosticListeners: HostDiagnosticListener[] = [];
2060
+ private readonly disconnectListeners: DisconnectListener[] = [];
1652
2061
  private readonly dropListeners: DropListener[] = [];
1653
2062
  private readonly hoverListeners: HoverListener[] = [];
1654
2063
  private readonly hoverLeftListeners: HoverLeftListener[] = [];
@@ -1720,7 +2129,9 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1720
2129
  return;
1721
2130
  }
1722
2131
  if (type === "hostError") {
1723
- console.error("AnQst host error:", message["payload"]);
2132
+ for (const listener of this.hostDiagnosticListeners) {
2133
+ listener(message["payload"]);
2134
+ }
1724
2135
  return;
1725
2136
  }
1726
2137
  if (type === "widgetReattached") {
@@ -1739,6 +2150,9 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1739
2150
  });
1740
2151
  }
1741
2152
  this.pending.clear();
2153
+ for (const listener of this.disconnectListeners) {
2154
+ listener();
2155
+ }
1742
2156
  });
1743
2157
  }
1744
2158
 
@@ -1807,6 +2221,14 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1807
2221
  this.slotListeners.push(handler);
1808
2222
  }
1809
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
+
1810
2232
  onDrop(handler: DropListener): void {
1811
2233
  this.dropListeners.push(handler);
1812
2234
  }
@@ -1822,53 +2244,116 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1822
2244
 
1823
2245
  @Injectable({ providedIn: "root" })
1824
2246
  class AnQstBridgeRuntime {
2247
+ private static readonly maxDiagnostics = 50;
1825
2248
  private adapter: BridgeAdapter | null = null;
1826
2249
  private readonly slotHandlers = new Map<string, SlotHandler>();
1827
2250
  private readonly outputHandlers = new Map<string, OutputHandler[]>();
1828
2251
  private readonly dropHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
1829
2252
  private readonly hoverHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
1830
2253
  private readonly hoverLeftHandlers = new Map<string, (() => void)[]>();
1831
- 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
+ }
1832
2281
 
1833
2282
  async ready(): Promise<void> {
1834
2283
  return this.startup;
1835
2284
  }
1836
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
+
1837
2295
  async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
1838
2296
  const adapter = await this.requireAdapter();
1839
2297
  return adapter.call<T>(service, member, args);
1840
2298
  }
1841
2299
 
1842
2300
  emit(service: string, member: string, args: unknown[]): void {
1843
- if (this.adapter !== null) {
1844
- this.adapter.emit(service, member, args);
1845
- return;
1846
- }
1847
- this.ready()
1848
- .then(() => this.requireAdapterSync().emit(service, member, args))
1849
- .catch((error) => console.error(error));
2301
+ this.publishNonCall("Emitter", service, member, (adapter) => adapter.emit(service, member, args));
1850
2302
  }
1851
2303
 
1852
2304
  setInput(service: string, member: string, value: unknown): void {
1853
- if (this.adapter !== null) {
1854
- this.adapter.setInput(service, member, value);
1855
- return;
1856
- }
1857
- this.ready()
1858
- .then(() => this.requireAdapterSync().setInput(service, member, value))
1859
- .catch((error) => console.error(error));
2305
+ this.publishNonCall("Input", service, member, (adapter) => adapter.setInput(service, member, value));
1860
2306
  }
1861
2307
 
1862
2308
  registerSlot(service: string, member: string, handler: SlotHandler): void {
1863
2309
  const key = this.key(service, member);
1864
2310
  this.slotHandlers.set(key, handler);
1865
2311
  if (this.adapter !== null) {
1866
- 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
+ }
1867
2326
  return;
1868
2327
  }
1869
2328
  this.ready()
1870
- .then(() => this.requireAdapterSync().registerSlot(service, member))
1871
- .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
+ });
1872
2357
  }
1873
2358
 
1874
2359
  onOutput(service: string, member: string, handler: OutputHandler): void {
@@ -1911,6 +2396,73 @@ class AnQstBridgeRuntime {
1911
2396
  return this.requireAdapterSync();
1912
2397
  }
1913
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
+
1914
2466
  private async init(): Promise<void> {
1915
2467
  const anyWindow = window as unknown as { qt?: { webChannelTransport?: unknown }; QWebChannel?: QWebChannelCtor };
1916
2468
  if (typeof anyWindow.QWebChannel === "function" && anyWindow.qt?.webChannelTransport !== undefined) {
@@ -1919,44 +2471,98 @@ class AnQstBridgeRuntime {
1919
2471
  this.adapter = await WebSocketBridgeAdapter.create();
1920
2472
  }
1921
2473
 
1922
- 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) => {
1923
2494
  const key = this.key(service, member);
1924
2495
  for (const outputHandler of this.outputHandlers.get(key) ?? []) {
1925
2496
  outputHandler(value);
1926
2497
  }
1927
2498
  });
1928
- this.adapter.onSlotInvocation(async (requestId, service, member, args) => {
2499
+ adapter.onSlotInvocation(async (requestId, service, member, args) => {
1929
2500
  const key = this.key(service, member);
1930
2501
  const handler = this.slotHandlers.get(key);
1931
2502
  if (handler === undefined) {
1932
- 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.");
1933
2515
  return;
1934
2516
  }
1935
2517
  try {
1936
2518
  const result = await Promise.resolve(handler(...args));
1937
2519
  if (result instanceof Error) {
1938
- 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);
1939
2534
  return;
1940
2535
  }
1941
- this.adapter!.resolveSlot(requestId, true, result, "");
2536
+ adapter.resolveSlot(requestId, true, result, "");
1942
2537
  } catch (error) {
1943
- const message = error instanceof Error ? error.message : String(error);
1944
- 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);
1945
2551
  }
1946
2552
  });
1947
- this.adapter.onDrop((service, member, payload, x, y) => {
2553
+ adapter.onDrop((service, member, payload, x, y) => {
1948
2554
  const key = this.key(service, member);
1949
2555
  for (const handler of this.dropHandlers.get(key) ?? []) {
1950
2556
  handler(payload, x, y);
1951
2557
  }
1952
2558
  });
1953
- this.adapter.onHover((service, member, payload, x, y) => {
2559
+ adapter.onHover((service, member, payload, x, y) => {
1954
2560
  const key = this.key(service, member);
1955
2561
  for (const handler of this.hoverHandlers.get(key) ?? []) {
1956
2562
  handler(payload, x, y);
1957
2563
  }
1958
2564
  });
1959
- this.adapter.onHoverLeft((service, member) => {
2565
+ adapter.onHoverLeft((service, member) => {
1960
2566
  const key = this.key(service, member);
1961
2567
  for (const handler of this.hoverLeftHandlers.get(key) ?? []) {
1962
2568
  handler();
@@ -1965,9 +2571,10 @@ class AnQstBridgeRuntime {
1965
2571
  for (const key of this.slotHandlers.keys()) {
1966
2572
  const parts = key.split("::");
1967
2573
  if (parts.length === 2) {
1968
- this.adapter.registerSlot(parts[0], parts[1]);
2574
+ adapter.registerSlot(parts[0], parts[1]);
1969
2575
  }
1970
2576
  }
2577
+ this._state.set("ready");
1971
2578
  }
1972
2579
 
1973
2580
  private key(service: string, member: string): string {
@@ -1975,6 +2582,23 @@ class AnQstBridgeRuntime {
1975
2582
  }
1976
2583
 
1977
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
+ }
1978
2602
  ${serviceClasses}
1979
2603
  `;
1980
2604
  }
@@ -1987,10 +2611,38 @@ function renderTsTypes(spec) {
1987
2611
  function renderTypeServicesDts(spec) {
1988
2612
  const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/services.d.ts`).trim();
1989
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
+ }`;
1990
2642
  const serviceDecls = spec.services
1991
2643
  .map((s) => renderTsServiceDts(spec, s.name))
1992
2644
  .join("\n\n");
1993
- const sections = [externalTypeImports, localTypeImports, serviceDecls.trim()].filter((s) => s.length > 0);
2645
+ const sections = [externalTypeImports, localTypeImports, bridgeDiagnosticsDecl, serviceDecls.trim()].filter((s) => s.length > 0);
1994
2646
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
1995
2647
  }
1996
2648
  function renderTypeTypesDts(spec) {
@@ -2066,6 +2718,7 @@ function renderNodeExpressWsTypes(spec) {
2066
2718
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
2067
2719
  }
2068
2720
  function renderNodeExpressWsIndex(spec) {
2721
+ const codecCatalog = (0, structured_top_level_codecs_1.buildStructuredCodecCatalog)(spec);
2069
2722
  const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
2070
2723
  const typeDecls = renderTypeDeclarations(spec, true);
2071
2724
  const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
@@ -2097,8 +2750,13 @@ function renderNodeExpressWsIndex(spec) {
2097
2750
  .map((member) => {
2098
2751
  const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
2099
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
+ : "[]";
2100
2758
  return ` ${service.name}_${member.name}(${args}${args ? ", " : ""}timeoutMs = this.defaultSlotTimeoutMs): Promise<${ret}> {
2101
- 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}`});
2102
2760
  }`;
2103
2761
  }))
2104
2762
  .join("\n");
@@ -2107,8 +2765,9 @@ function renderNodeExpressWsIndex(spec) {
2107
2765
  .filter((member) => member.kind === "Output" && member.payloadTypeText)
2108
2766
  .map((member) => {
2109
2767
  const typeText = mapTypeTextToTs(member.payloadTypeText);
2768
+ const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
2110
2769
  return ` set${service.name}_${nodeCap(member.name)}(value: ${typeText}): void {
2111
- this.setOutputValue("${service.name}", "${member.name}", value);
2770
+ this.setOutputValue("${service.name}", "${member.name}", ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"});
2112
2771
  }`;
2113
2772
  }))
2114
2773
  .join("\n");
@@ -2161,8 +2820,9 @@ function renderNodeExpressWsIndex(spec) {
2161
2820
  .filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
2162
2821
  .map((member) => {
2163
2822
  const typeText = mapTypeTextToTs(member.payloadTypeText);
2823
+ const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
2164
2824
  if (member.kind === "Input") {
2165
- 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 },`;
2166
2826
  }
2167
2827
  return ` ${member.name}: {\n set: (value: ${typeText}) => session.set${service.name}_${nodeCap(member.name)}(value)\n },`;
2168
2828
  })
@@ -2174,6 +2834,11 @@ function renderNodeExpressWsIndex(spec) {
2174
2834
  .flatMap((service) => service.members
2175
2835
  .filter((member) => member.kind === "Call" && member.payloadTypeText)
2176
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
+ : "";
2177
2842
  return ` if (service === "${service.name}" && member === "${member.name}") {
2178
2843
  const handler = implementation.${service.name}.${member.name};
2179
2844
  if (typeof handler !== "function") {
@@ -2196,8 +2861,8 @@ function renderNodeExpressWsIndex(spec) {
2196
2861
  });
2197
2862
  throw err;
2198
2863
  }
2199
- Promise.resolve(handler(buildHandlerBridge(session), ...(args as ${nodeParamTuple(member)})))
2200
- .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"} }))
2201
2866
  .catch((error) => {
2202
2867
  const message = error instanceof Error ? error.message : String(error);
2203
2868
  emitDiagnostic({
@@ -2225,6 +2890,13 @@ function renderNodeExpressWsIndex(spec) {
2225
2890
  .flatMap((service) => service.members
2226
2891
  .filter((member) => member.kind === "Emitter")
2227
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
+ : "[]";
2228
2900
  return ` if (service === "${service.name}" && member === "${member.name}") {
2229
2901
  const handler = implementation.${service.name}.${member.name};
2230
2902
  if (typeof handler !== "function") {
@@ -2241,7 +2913,8 @@ function renderNodeExpressWsIndex(spec) {
2241
2913
  });
2242
2914
  throw err;
2243
2915
  }
2244
- 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) => {
2245
2918
  const message = error instanceof Error ? error.message : String(error);
2246
2919
  emitDiagnostic({
2247
2920
  code: "EmitterHandlerError",
@@ -2262,6 +2935,7 @@ function renderNodeExpressWsIndex(spec) {
2262
2935
  .flatMap((service) => service.members
2263
2936
  .filter((member) => member.kind === "Input" && member.payloadTypeText)
2264
2937
  .map((member) => {
2938
+ const payloadSite = (0, structured_top_level_codecs_1.getStructuredPayloadSite)(codecCatalog, service.name, member.name);
2265
2939
  return ` if (service === "${service.name}" && member === "${member.name}") {
2266
2940
  const handler = implementation.${service.name}.${member.name};
2267
2941
  if (typeof handler !== "function") {
@@ -2278,7 +2952,9 @@ function renderNodeExpressWsIndex(spec) {
2278
2952
  });
2279
2953
  throw err;
2280
2954
  }
2281
- 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) => {
2282
2958
  const message = error instanceof Error ? error.message : String(error);
2283
2959
  emitDiagnostic({
2284
2960
  code: "InputHandlerError",
@@ -2300,6 +2976,9 @@ import type { WebSocket, WebSocketServer } from "ws";
2300
2976
  ${typeImports}
2301
2977
  ${typeDecls}
2302
2978
 
2979
+ // Structured/top-level codec helpers
2980
+ ${(0, structured_top_level_codecs_1.renderTsStructuredCodecHelpers)(codecCatalog)}
2981
+
2303
2982
  ${handlerInterfaces}
2304
2983
 
2305
2984
  export interface ${spec.widgetName}NodeImplementation {
@@ -2666,7 +3345,6 @@ ${callDispatch}
2666
3345
  const service = String(message.service ?? "");
2667
3346
  const member = String(message.member ?? "");
2668
3347
  const args = Array.isArray(message.args) ? (message.args as unknown[]) : [];
2669
- session.emitSignal(service, member, args);
2670
3348
  ${emitterDispatch}
2671
3349
  const err = new Error(\`No Emitter mapping found for \${service}.\${member}\`);
2672
3350
  emitDiagnostic({
@@ -2685,7 +3363,6 @@ ${emitterDispatch}
2685
3363
  const service = String(message.service ?? "");
2686
3364
  const member = String(message.member ?? "");
2687
3365
  const value = message.value;
2688
- session.setInputState(service, member, value);
2689
3366
  ${inputDispatch}
2690
3367
  const err = new Error(\`No Input mapping found for \${service}.\${member}\`);
2691
3368
  emitDiagnostic({
@@ -2979,13 +3656,15 @@ function renderQtIntegrationCMake(widgetName) {
2979
3656
  const generatedRootVar = "ANQST_GENERATED_WIDGET_DIR";
2980
3657
  const generatedIncludeVar = "ANQST_GENERATED_INCLUDE_DIR";
2981
3658
  const projectRootVar = "ANQST_PROJECT_ROOT";
3659
+ const requiredFilesVar = "ANQST_REQUIRED_GENERATED_FILES";
3660
+ const widgetBinaryDirVar = "ANQST_GENERATED_WIDGET_BINARY_DIR";
2982
3661
  const widgetTarget = `${widgetName}Widget`;
2983
- const autogenTarget = `${widgetTarget}_anqst_codegen`;
2984
3662
  return `cmake_minimum_required(VERSION 3.21)
2985
3663
 
2986
3664
  set(${projectRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../../../../..")
2987
3665
  set(${generatedRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../qt/${generatedCppLibraryDirName(widgetName)}")
2988
3666
  set(${generatedIncludeVar} "\${${generatedRootVar}}/include")
3667
+ set(${widgetBinaryDirVar} "\${CMAKE_CURRENT_BINARY_DIR}/${generatedCppLibraryDirName(widgetName)}")
2989
3668
 
2990
3669
  if(TARGET ${widgetTarget})
2991
3670
  return()
@@ -2995,66 +3674,27 @@ if(NOT TARGET anqstwebhostbase)
2995
3674
  message(FATAL_ERROR "Target 'anqstwebhostbase' must exist before including generated AnQst CMake for ${widgetName}.")
2996
3675
  endif()
2997
3676
 
2998
- find_package(Qt5 REQUIRED COMPONENTS Core Widgets)
2999
- set(CMAKE_AUTOMOC ON)
3000
- set(CMAKE_AUTOUIC ON)
3001
- set(CMAKE_AUTORCC ON)
3002
-
3003
- find_program(ANQST_NPM_EXECUTABLE npm REQUIRED)
3004
- find_program(ANQST_NPX_EXECUTABLE npx REQUIRED)
3005
-
3006
- add_custom_command(
3007
- OUTPUT
3008
- "\${${generatedRootVar}}/CMakeLists.txt"
3009
- "\${${generatedRootVar}}/${widgetName}.qrc"
3010
- "\${${generatedRootVar}}/${widgetName}.cpp"
3011
- "\${${generatedIncludeVar}}/${widgetName}.h"
3012
- "\${${generatedIncludeVar}}/${widgetName}Widget.h"
3013
- "\${${generatedIncludeVar}}/${widgetName}Types.h"
3014
- "\${${generatedRootVar}}/webapp/index.html"
3015
- COMMAND "\${ANQST_NPM_EXECUTABLE}" install
3016
- COMMAND "\${ANQST_NPX_EXECUTABLE}" anqst build
3017
- WORKING_DIRECTORY "\${${projectRootVar}}"
3018
- COMMENT "Generating AnQst widget library (${widgetTarget}) from Angular project"
3019
- VERBATIM
3020
- )
3021
-
3022
- add_custom_target(${autogenTarget}
3023
- DEPENDS
3024
- "\${${generatedRootVar}}/CMakeLists.txt"
3025
- "\${${generatedRootVar}}/${widgetName}.qrc"
3026
- "\${${generatedRootVar}}/${widgetName}.cpp"
3027
- "\${${generatedIncludeVar}}/${widgetName}.h"
3028
- "\${${generatedIncludeVar}}/${widgetName}Widget.h"
3029
- "\${${generatedIncludeVar}}/${widgetName}Types.h"
3030
- "\${${generatedRootVar}}/webapp/index.html"
3031
- )
3032
-
3033
- set_source_files_properties(
3677
+ set(${requiredFilesVar}
3678
+ "\${${generatedRootVar}}/CMakeLists.txt"
3034
3679
  "\${${generatedRootVar}}/${widgetName}.qrc"
3035
3680
  "\${${generatedRootVar}}/${widgetName}.cpp"
3036
3681
  "\${${generatedIncludeVar}}/${widgetName}.h"
3037
3682
  "\${${generatedIncludeVar}}/${widgetName}Widget.h"
3038
3683
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
3039
- PROPERTIES GENERATED TRUE
3684
+ "\${${generatedRootVar}}/webapp/index.html"
3040
3685
  )
3041
3686
 
3042
- add_library(${widgetTarget}
3043
- "\${${generatedRootVar}}/${widgetName}.qrc"
3044
- "\${${generatedRootVar}}/${widgetName}.cpp"
3045
- "\${${generatedIncludeVar}}/${widgetName}.h"
3046
- "\${${generatedIncludeVar}}/${widgetName}Widget.h"
3047
- "\${${generatedIncludeVar}}/${widgetName}Types.h"
3048
- )
3049
- add_dependencies(${widgetTarget} ${autogenTarget})
3050
- target_include_directories(${widgetTarget}
3051
- PUBLIC
3052
- "\${${generatedIncludeVar}}"
3053
- )
3054
- target_link_libraries(${widgetTarget}
3055
- PUBLIC
3056
- anqstwebhostbase
3057
- )
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}}")
3058
3698
  `;
3059
3699
  }
3060
3700
  function installQtIntegrationCMake(cwd, widgetName) {
@@ -3273,7 +3913,7 @@ public:
3273
3913
  " <widget class=\\"${widgetClass}\\" name=\\"${widgetName.toLowerCase()}\\">\\n"
3274
3914
  " <property name=\\"minimumSize\\">\\n"
3275
3915
  " <size>\\n"
3276
- " <width>256</width>\\n"
3916
+ " <width>0</width>\\n"
3277
3917
  " <height>128</height>\\n"
3278
3918
  " </size>\\n"
3279
3919
  " </property>\\n"