@dusted/anqst 1.0.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 boundary_codecs_1 = require("./boundary-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,129 @@ 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
+ }
86
+ function collectFiniteStringLiteralsTypeNode(node) {
87
+ const values = [];
88
+ for (const part of node.types) {
89
+ if (!typescript_1.default.isLiteralTypeNode(part) || !typescript_1.default.isStringLiteral(part.literal))
90
+ return null;
91
+ values.push(part.literal.text);
92
+ }
93
+ return values;
94
+ }
95
+ function collectFiniteBooleanLiteralsTypeNode(node) {
96
+ const values = [];
97
+ for (const part of node.types) {
98
+ if (!typescript_1.default.isLiteralTypeNode(part))
99
+ return null;
100
+ if (part.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword) {
101
+ values.push(true);
102
+ continue;
103
+ }
104
+ if (part.literal.kind === typescript_1.default.SyntaxKind.FalseKeyword) {
105
+ values.push(false);
106
+ continue;
107
+ }
108
+ return null;
109
+ }
110
+ return values;
111
+ }
112
+ function collectFiniteNumberLiteralsTypeNode(node) {
113
+ const values = [];
114
+ for (const part of node.types) {
115
+ if (!typescript_1.default.isLiteralTypeNode(part) || !typescript_1.default.isNumericLiteral(part.literal))
116
+ return null;
117
+ values.push(Number(part.literal.text));
118
+ }
119
+ return values;
120
+ }
121
+ function finiteDomainSymbolForCpp(value) {
122
+ if (typeof value === "boolean")
123
+ return value ? "True" : "False";
124
+ if (typeof value === "number") {
125
+ const text = Number.isInteger(value) ? `${value}` : `${value}`.replace(/\./g, "_");
126
+ return sanitizeIdentifier(`Value_${text.replace(/-/g, "neg_")}`);
127
+ }
128
+ const direct = sanitizeIdentifier(value.trim());
129
+ return direct.length > 0 ? direct : "Value";
130
+ }
131
+ function buildCppFiniteDomain(primitive, values) {
132
+ const seen = new Set();
133
+ const variants = [];
134
+ for (const value of values) {
135
+ const key = `${typeof value}:${String(value)}`;
136
+ if (seen.has(key))
137
+ continue;
138
+ seen.add(key);
139
+ variants.push({
140
+ code: variants.length,
141
+ symbolicName: finiteDomainSymbolForCpp(value),
142
+ value
143
+ });
144
+ }
145
+ return { primitive, variants };
146
+ }
147
+ function collectFiniteDomainFromTypeNode(typeNode) {
148
+ if (typescript_1.default.isParenthesizedTypeNode(typeNode)) {
149
+ return collectFiniteDomainFromTypeNode(typeNode.type);
150
+ }
151
+ if (typescript_1.default.isLiteralTypeNode(typeNode)) {
152
+ if (typescript_1.default.isStringLiteral(typeNode.literal)) {
153
+ return buildCppFiniteDomain("string", [typeNode.literal.text]);
154
+ }
155
+ if (typescript_1.default.isNumericLiteral(typeNode.literal)) {
156
+ return buildCppFiniteDomain("number", [Number(typeNode.literal.text)]);
157
+ }
158
+ if (typeNode.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword || typeNode.literal.kind === typescript_1.default.SyntaxKind.FalseKeyword) {
159
+ return buildCppFiniteDomain("boolean", [typeNode.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword]);
160
+ }
161
+ return null;
162
+ }
163
+ if (!typescript_1.default.isUnionTypeNode(typeNode))
164
+ return null;
165
+ const filtered = filterNullishUnionTypeNodes(typeNode.types);
166
+ if (filtered.length !== typeNode.types.length)
167
+ return null;
168
+ const finiteStrings = collectFiniteStringLiteralsTypeNode(typeNode);
169
+ if (finiteStrings)
170
+ return buildCppFiniteDomain("string", finiteStrings);
171
+ const finiteBooleans = collectFiniteBooleanLiteralsTypeNode(typeNode);
172
+ if (finiteBooleans)
173
+ return buildCppFiniteDomain("boolean", finiteBooleans);
174
+ const finiteNumbers = collectFiniteNumberLiteralsTypeNode(typeNode);
175
+ if (finiteNumbers)
176
+ return buildCppFiniteDomain("number", finiteNumbers);
177
+ return null;
178
+ }
34
179
  function mapTsTypeToCpp(typeText) {
35
180
  const raw = typeText.trim();
36
181
  if (/\bAnQst\.Type\.qint64\b/.test(raw))
@@ -55,6 +200,9 @@ function mapTsTypeToCpp(typeText) {
55
200
  return "QString";
56
201
  if (/\bAnQst\.Type\.json\b/.test(raw) || /\bAnQst\.Type\.object\b/.test(raw))
57
202
  return "QVariantMap";
203
+ if (/\bAnQst\.Type\.(?:buffer|blob|typedArray|uint8Array|int8Array|uint16Array|int16Array|uint32Array|int32Array|float32Array|float64Array)\b/.test(raw)) {
204
+ return "QByteArray";
205
+ }
58
206
  if (/\bAnQst\.Type\.(u?int(8|16|32))\b/.test(raw)) {
59
207
  const narrowed = raw.match(/\bAnQst\.Type\.(u?int(?:8|16|32))\b/)?.[1];
60
208
  if (narrowed === "int8")
@@ -83,6 +231,20 @@ function mapTsTypeToCpp(typeText) {
83
231
  return "void";
84
232
  if (t === "object")
85
233
  return "QVariantMap";
234
+ if (t === "ArrayBuffer")
235
+ return "QByteArray";
236
+ if ([
237
+ "Uint8Array",
238
+ "Int8Array",
239
+ "Uint16Array",
240
+ "Int16Array",
241
+ "Uint32Array",
242
+ "Int32Array",
243
+ "Float32Array",
244
+ "Float64Array"
245
+ ].includes(t)) {
246
+ return "QByteArray";
247
+ }
86
248
  if (t.endsWith("[]")) {
87
249
  return `QList<${mapTsTypeToCpp(t.slice(0, -2))}>`;
88
250
  }
@@ -110,6 +272,11 @@ function callbackName(memberName) {
110
272
  function pascalCase(value) {
111
273
  return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
112
274
  }
275
+ function sanitizeIdentifier(value) {
276
+ const trimmed = value.replace(/[^A-Za-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
277
+ const withFallback = trimmed.length > 0 ? trimmed : "Codec";
278
+ return /^[0-9]/.test(withFallback) ? `T_${withFallback}` : withFallback;
279
+ }
113
280
  function variantToCppExpression(cppType, expr) {
114
281
  if (cppType === "QString")
115
282
  return `${expr}.toString()`;
@@ -117,6 +284,8 @@ function variantToCppExpression(cppType, expr) {
117
284
  return `${expr}.toStringList()`;
118
285
  if (cppType === "QVariantMap")
119
286
  return `${expr}.toMap()`;
287
+ if (cppType === "QByteArray")
288
+ return `${expr}.toByteArray()`;
120
289
  if (cppType === "double")
121
290
  return `${expr}.toDouble()`;
122
291
  if (cppType === "bool")
@@ -140,6 +309,7 @@ function cppToVariantExpression(cppType, expr) {
140
309
  if (cppType === "QString" ||
141
310
  cppType === "QStringList" ||
142
311
  cppType === "QVariantMap" ||
312
+ cppType === "QByteArray" ||
143
313
  cppType === "double" ||
144
314
  cppType === "bool" ||
145
315
  cppType === "qint64" ||
@@ -398,6 +568,8 @@ class CppTypeNormalizer {
398
568
  this.seedOrder = [];
399
569
  this.allKnownNames = new Set();
400
570
  this.usedNames = new Set();
571
+ this.syntheticNameByKey = new Map();
572
+ this.finiteDomainNameByKey = new Map();
401
573
  for (const decl of collectStructDecls(spec)) {
402
574
  this.allKnownNames.add(decl.name);
403
575
  this.usedNames.add(decl.name);
@@ -421,9 +593,11 @@ class CppTypeNormalizer {
421
593
  const order = this.topologicalOrder();
422
594
  const orderedDecls = order.map((name) => this.declMap.get(name)).filter((x) => !!x);
423
595
  const structNames = orderedDecls.filter((d) => d.kind === "struct").map((d) => d.name);
596
+ const metatypeNames = orderedDecls.filter((d) => d.kind === "struct" || d.kind === "enum").map((d) => d.name);
424
597
  return {
425
598
  orderedDecls,
426
599
  structNames,
600
+ metatypeNames,
427
601
  mapTypeText: (typeText, nameHintParts) => this.mapTypeText(typeText, nameHintParts)
428
602
  };
429
603
  }
@@ -441,7 +615,19 @@ class CppTypeNormalizer {
441
615
  optional: !!member.questionToken
442
616
  });
443
617
  }
444
- return { name, kind: "struct", fields, aliasType: null, deps, isUnionAlias: false };
618
+ return { name, kind: "struct", fields, aliasType: null, deps, isUnionAlias: false, finiteDomain: null };
619
+ }
620
+ const finiteDomain = collectFiniteDomainFromTypeNode(node.type);
621
+ if (finiteDomain) {
622
+ return {
623
+ name,
624
+ kind: "enum",
625
+ fields: [],
626
+ aliasType: null,
627
+ deps: new Set(),
628
+ isUnionAlias: false,
629
+ finiteDomain
630
+ };
445
631
  }
446
632
  const deps = new Set();
447
633
  const aliasType = this.mapTypeNode(node.type, [name], deps);
@@ -451,14 +637,29 @@ class CppTypeNormalizer {
451
637
  fields: [],
452
638
  aliasType,
453
639
  deps,
454
- isUnionAlias: node.type.getText().includes("|")
640
+ isUnionAlias: node.type.getText().includes("|"),
641
+ finiteDomain: null
455
642
  };
456
643
  }
457
644
  mapTypeNode(typeNode, nameHintParts, deps) {
458
645
  if (typescript_1.default.isParenthesizedTypeNode(typeNode)) {
459
646
  return this.mapTypeNode(typeNode.type, nameHintParts, deps);
460
647
  }
648
+ const finiteDomain = collectFiniteDomainFromTypeNode(typeNode);
649
+ if (finiteDomain) {
650
+ return this.ensureFiniteDomainType(finiteDomain, nameHintParts, deps);
651
+ }
461
652
  if (typescript_1.default.isUnionTypeNode(typeNode)) {
653
+ const filtered = filterNullishUnionTypeNodes(typeNode.types);
654
+ if (filtered.length === 1) {
655
+ return this.mapTypeNode(filtered[0], nameHintParts, deps);
656
+ }
657
+ if (isStringLikeUnionTypeNode(typeNode))
658
+ return "QString";
659
+ if (isBooleanLikeUnionTypeNode(typeNode))
660
+ return "bool";
661
+ if (isNumberLikeUnionTypeNode(typeNode))
662
+ return "double";
462
663
  return "QString";
463
664
  }
464
665
  if (typescript_1.default.isTypeLiteralNode(typeNode)) {
@@ -504,9 +705,44 @@ class CppTypeNormalizer {
504
705
  this.collectKnownTypeDeps(mapped, deps);
505
706
  return mapped;
506
707
  }
708
+ ensureFiniteDomainType(domain, nameHintParts, deps) {
709
+ const baseName = this.makeSyntheticBaseName(nameHintParts);
710
+ const domainKey = `${baseName}::finite::${domain.primitive}::${domain.variants.map((variant) => `${variant.symbolicName}=${String(variant.value)}`).join("|")}`;
711
+ const existingName = this.finiteDomainNameByKey.get(domainKey);
712
+ if (existingName) {
713
+ deps.add(existingName);
714
+ return existingName;
715
+ }
716
+ const synthesizedName = this.allocateUniqueName(baseName);
717
+ this.finiteDomainNameByKey.set(domainKey, synthesizedName);
718
+ if (this.declMap.has(synthesizedName)) {
719
+ deps.add(synthesizedName);
720
+ return synthesizedName;
721
+ }
722
+ this.allKnownNames.add(synthesizedName);
723
+ this.declMap.set(synthesizedName, {
724
+ name: synthesizedName,
725
+ kind: "enum",
726
+ fields: [],
727
+ aliasType: null,
728
+ deps: new Set(),
729
+ isUnionAlias: false,
730
+ finiteDomain: domain
731
+ });
732
+ this.seedOrder.push(synthesizedName);
733
+ deps.add(synthesizedName);
734
+ return synthesizedName;
735
+ }
507
736
  ensureSyntheticStruct(typeNode, nameHintParts, deps) {
508
737
  const baseName = this.makeSyntheticBaseName(nameHintParts);
738
+ const syntheticKey = `${baseName}::${typeNode.getText()}`;
739
+ const existingName = this.syntheticNameByKey.get(syntheticKey);
740
+ if (existingName) {
741
+ deps.add(existingName);
742
+ return existingName;
743
+ }
509
744
  const synthesizedName = this.allocateUniqueName(baseName);
745
+ this.syntheticNameByKey.set(syntheticKey, synthesizedName);
510
746
  if (this.declMap.has(synthesizedName)) {
511
747
  deps.add(synthesizedName);
512
748
  return synthesizedName;
@@ -530,7 +766,8 @@ class CppTypeNormalizer {
530
766
  fields,
531
767
  aliasType: null,
532
768
  deps: localDeps,
533
- isUnionAlias: false
769
+ isUnionAlias: false,
770
+ finiteDomain: null
534
771
  });
535
772
  this.seedOrder.push(synthesizedName);
536
773
  deps.add(synthesizedName);
@@ -592,6 +829,17 @@ class CppTypeNormalizer {
592
829
  }
593
830
  }
594
831
  function renderCppDecl(decl) {
832
+ if (decl.kind === "enum") {
833
+ const variants = decl.finiteDomain?.variants ?? [];
834
+ const underlyingType = variants.length <= 0xff ? "std::uint8_t" : variants.length <= 0xffff ? "std::uint16_t" : "std::uint32_t";
835
+ const lines = [];
836
+ lines.push(`enum class ${decl.name} : ${underlyingType} {`);
837
+ for (const variant of variants) {
838
+ lines.push(` ${variant.symbolicName} = ${variant.code},`);
839
+ }
840
+ lines.push("};");
841
+ return lines.join("\n");
842
+ }
595
843
  if (decl.kind === "alias") {
596
844
  if (decl.isUnionAlias && decl.aliasType === "QString") {
597
845
  return `using ${decl.name} = QString; // union mapped conservatively`;
@@ -647,9 +895,247 @@ function collectDragDropMimeConstants(spec) {
647
895
  }
648
896
  return constants;
649
897
  }
898
+ function createCarrierSummary(counts, singleKinds = [], mayBlob = false, mustBlob = false) {
899
+ return {
900
+ counts: new Set(counts),
901
+ singleKinds: new Set(singleKinds),
902
+ mayBlob,
903
+ mustBlob
904
+ };
905
+ }
906
+ function addOptionalAbsence(summary) {
907
+ const counts = new Set(summary.counts);
908
+ counts.add(0);
909
+ return {
910
+ counts,
911
+ singleKinds: new Set(summary.singleKinds),
912
+ mayBlob: summary.mayBlob,
913
+ mustBlob: summary.mustBlob
914
+ };
915
+ }
916
+ function saturatingItemCountAdd(left, right) {
917
+ if (left === 2 || right === 2)
918
+ return 2;
919
+ const total = left + right;
920
+ return total >= 2 ? 2 : total;
921
+ }
922
+ function mergeCarrierSummaries(left, right) {
923
+ const counts = new Set();
924
+ const singleKinds = new Set();
925
+ for (const leftCount of left.counts) {
926
+ for (const rightCount of right.counts) {
927
+ const total = saturatingItemCountAdd(leftCount, rightCount);
928
+ counts.add(total);
929
+ if (total !== 1)
930
+ continue;
931
+ if (leftCount === 1 && rightCount === 0) {
932
+ for (const kind of left.singleKinds)
933
+ singleKinds.add(kind);
934
+ }
935
+ if (leftCount === 0 && rightCount === 1) {
936
+ for (const kind of right.singleKinds)
937
+ singleKinds.add(kind);
938
+ }
939
+ }
940
+ }
941
+ return {
942
+ counts,
943
+ singleKinds,
944
+ mayBlob: left.mayBlob || right.mayBlob,
945
+ mustBlob: left.mustBlob || right.mustBlob
946
+ };
947
+ }
948
+ function summarizePlanCarrier(node) {
949
+ switch (node.nodeKind) {
950
+ case "leaf":
951
+ if (node.blobEntryId)
952
+ return createCarrierSummary([0], [], true, true);
953
+ if (node.itemEntryId) {
954
+ return createCarrierSummary([1], [node.leaf.region === "dynamic" ? "object" : "string"]);
955
+ }
956
+ return createCarrierSummary([0]);
957
+ case "named":
958
+ return summarizePlanCarrier(node.target);
959
+ case "finite-domain":
960
+ if (node.blobEntryId)
961
+ return createCarrierSummary([0], [], true, true);
962
+ if (node.itemEntryId)
963
+ return createCarrierSummary([1], ["string"]);
964
+ return createCarrierSummary([0]);
965
+ case "array": {
966
+ if (node.extentStrategy === "blob-tail") {
967
+ return createCarrierSummary([0], [], true, false);
968
+ }
969
+ const elementSummary = summarizePlanCarrier(node.element);
970
+ const counts = new Set([0]);
971
+ const singleKinds = new Set();
972
+ if (elementSummary.counts.has(1)) {
973
+ counts.add(1);
974
+ for (const kind of elementSummary.singleKinds)
975
+ singleKinds.add(kind);
976
+ }
977
+ if (elementSummary.counts.has(1) || elementSummary.counts.has(2)) {
978
+ counts.add(2);
979
+ }
980
+ return {
981
+ counts,
982
+ singleKinds,
983
+ mayBlob: true,
984
+ mustBlob: true
985
+ };
986
+ }
987
+ case "struct": {
988
+ let summary = createCarrierSummary([0]);
989
+ for (const field of node.fields) {
990
+ let fieldSummary = summarizePlanCarrier(field.node);
991
+ if (field.optional) {
992
+ fieldSummary = addOptionalAbsence(fieldSummary);
993
+ }
994
+ if (field.presenceStrategy) {
995
+ fieldSummary = {
996
+ counts: new Set(fieldSummary.counts),
997
+ singleKinds: new Set(fieldSummary.singleKinds),
998
+ mayBlob: true,
999
+ mustBlob: true
1000
+ };
1001
+ }
1002
+ summary = mergeCarrierSummaries(summary, fieldSummary);
1003
+ }
1004
+ return summary;
1005
+ }
1006
+ }
1007
+ }
1008
+ function inferDragDropCarrierKinds(plan) {
1009
+ const summary = summarizePlanCarrier(plan.root);
1010
+ const carriers = new Set();
1011
+ const addNoBlobCarriers = () => {
1012
+ if (summary.counts.has(0))
1013
+ carriers.add("array");
1014
+ if (summary.counts.has(1)) {
1015
+ for (const kind of summary.singleKinds)
1016
+ carriers.add(kind);
1017
+ }
1018
+ if (summary.counts.has(2))
1019
+ carriers.add("array");
1020
+ };
1021
+ if (summary.mustBlob) {
1022
+ if (summary.counts.has(0))
1023
+ carriers.add("string");
1024
+ if (summary.counts.has(1) || summary.counts.has(2))
1025
+ carriers.add("array");
1026
+ }
1027
+ else {
1028
+ addNoBlobCarriers();
1029
+ if (summary.mayBlob) {
1030
+ if (summary.counts.has(0))
1031
+ carriers.add("string");
1032
+ if (summary.counts.has(1) || summary.counts.has(2))
1033
+ carriers.add("array");
1034
+ }
1035
+ }
1036
+ return ["string", "array", "object"].filter((kind) => carriers.has(kind));
1037
+ }
1038
+ function collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog) {
1039
+ const seen = new Set();
1040
+ const helpers = [];
1041
+ for (const service of spec.services) {
1042
+ for (const member of service.members) {
1043
+ if ((member.kind !== "DropTarget" && member.kind !== "HoverTarget") || !member.payloadTypeText)
1044
+ continue;
1045
+ const typeName = member.payloadTypeText.replace(/\s/g, "");
1046
+ if (seen.has(typeName))
1047
+ continue;
1048
+ seen.add(typeName);
1049
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
1050
+ if (!payloadSite)
1051
+ continue;
1052
+ const plan = cppCodecCatalog.plansByCodecId.get(payloadSite.codecId);
1053
+ if (!plan)
1054
+ continue;
1055
+ helpers.push({
1056
+ typeName,
1057
+ cppType: cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]),
1058
+ codecId: payloadSite.codecId,
1059
+ carriers: inferDragDropCarrierKinds(plan)
1060
+ });
1061
+ }
1062
+ }
1063
+ return helpers;
1064
+ }
1065
+ function collectTsDragDropPayloadHelpers(spec, codecCatalog) {
1066
+ const seen = new Set();
1067
+ const helpers = [];
1068
+ for (const service of spec.services) {
1069
+ for (const member of service.members) {
1070
+ if ((member.kind !== "DropTarget" && member.kind !== "HoverTarget") || !member.payloadTypeText)
1071
+ continue;
1072
+ const typeName = member.payloadTypeText.replace(/\s/g, "");
1073
+ if (seen.has(typeName))
1074
+ continue;
1075
+ seen.add(typeName);
1076
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
1077
+ if (!payloadSite)
1078
+ continue;
1079
+ const plan = codecCatalog.plansByCodecId.get(payloadSite.codecId);
1080
+ if (!plan)
1081
+ continue;
1082
+ helpers.push({
1083
+ typeName,
1084
+ tsType: mapTypeTextToTs(member.payloadTypeText),
1085
+ codecId: payloadSite.codecId,
1086
+ carriers: inferDragDropCarrierKinds(plan)
1087
+ });
1088
+ }
1089
+ }
1090
+ return helpers;
1091
+ }
1092
+ function renderTsDragDropPayloadHelpers(spec, codecCatalog) {
1093
+ const helpers = collectTsDragDropPayloadHelpers(spec, codecCatalog);
1094
+ return helpers
1095
+ .map((helper) => {
1096
+ const lines = [
1097
+ `function decodeDragDropPayload_${helper.typeName}(rawPayload: unknown): ${helper.tsType} {`,
1098
+ ` if (typeof rawPayload !== "string") {`,
1099
+ ` throw new Error("Drag/drop payload must be tagged text.");`,
1100
+ ` }`,
1101
+ ` if (rawPayload.length === 0) {`,
1102
+ ` throw new Error("Drag/drop payload is empty.");`,
1103
+ ` }`,
1104
+ ` const transportTag = rawPayload[0];`,
1105
+ ` const payloadText = rawPayload.slice(1);`
1106
+ ];
1107
+ if (helper.carriers.includes("string")) {
1108
+ lines.push(` if (transportTag === "S") {`);
1109
+ lines.push(` return decode${helper.codecId}(payloadText);`);
1110
+ lines.push(` }`);
1111
+ }
1112
+ if (helper.carriers.includes("array")) {
1113
+ lines.push(` if (transportTag === "A") {`);
1114
+ lines.push(` const parsed = JSON.parse(payloadText) as unknown;`);
1115
+ lines.push(` if (!Array.isArray(parsed)) {`);
1116
+ lines.push(` throw new Error("Drag/drop payload must be a JSON array.");`);
1117
+ lines.push(` }`);
1118
+ lines.push(` return decode${helper.codecId}(parsed);`);
1119
+ lines.push(` }`);
1120
+ }
1121
+ if (helper.carriers.includes("object")) {
1122
+ lines.push(` if (transportTag === "O") {`);
1123
+ lines.push(` const parsed = JSON.parse(payloadText) as unknown;`);
1124
+ lines.push(` if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {`);
1125
+ lines.push(` throw new Error("Drag/drop payload must be a JSON object.");`);
1126
+ lines.push(` }`);
1127
+ lines.push(` return decode${helper.codecId}(parsed);`);
1128
+ lines.push(` }`);
1129
+ }
1130
+ lines.push(` throw new Error(\`Drag/drop payload has an unknown transport tag: \${transportTag}\`);`);
1131
+ lines.push(`}`);
1132
+ return lines.join("\n");
1133
+ })
1134
+ .join("\n\n");
1135
+ }
650
1136
  function renderTypesHeader(spec, cppTypes) {
651
1137
  const decls = cppTypes.orderedDecls.map(renderCppDecl).join("\n\n");
652
- const metatypes = cppTypes.structNames
1138
+ const metatypes = cppTypes.metatypeNames
653
1139
  .flatMap((name) => [
654
1140
  `Q_DECLARE_METATYPE(${spec.widgetName}::${name})`,
655
1141
  `Q_DECLARE_METATYPE(QList<${spec.widgetName}::${name}>)`
@@ -663,6 +1149,7 @@ function renderTypesHeader(spec, cppTypes) {
663
1149
  return `#pragma once
664
1150
  #include <QString>
665
1151
  #include <QStringList>
1152
+ #include <QByteArray>
666
1153
  #include <QList>
667
1154
  #include <QVariantMap>
668
1155
  #include <QMetaType>
@@ -680,13 +1167,13 @@ ${metatypes}
680
1167
  }
681
1168
  function renderWidgetUmbrellaHeader(spec) {
682
1169
  return `#pragma once
683
- // Built by <AnQst_version>
684
1170
  #include "${spec.widgetName}Widget.h"
685
1171
  #include "${spec.widgetName}Types.h"
686
1172
  `;
687
1173
  }
688
- function renderWidgetHeader(spec, cppTypes) {
1174
+ function renderWidgetHeader(spec, cppTypes, cppCodecCatalog) {
689
1175
  const widgetClassName = `${spec.widgetName}Widget`;
1176
+ const dragDropPayloadHelpers = collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog);
690
1177
  const callbackAliases = [];
691
1178
  const publicMethods = [];
692
1179
  const slotMethods = [];
@@ -696,10 +1183,12 @@ function renderWidgetHeader(spec, cppTypes) {
696
1183
  const properties = [];
697
1184
  const fields = [];
698
1185
  const publicSlots = [];
699
- const bindings = [];
1186
+ const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) => [
1187
+ `static QByteArray encodeDragDropPayload_${helper.typeName}(const ${helper.cppType}& payload);`,
1188
+ `static std::optional<${helper.cppType}> decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload);`
1189
+ ]);
700
1190
  for (const service of spec.services) {
701
1191
  for (const member of service.members) {
702
- bindings.push({ service: service.name, member: member.name, kind: member.kind });
703
1192
  const memberPascal = pascalCase(member.name);
704
1193
  if (member.kind === "Call" && member.payloadTypeText) {
705
1194
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
@@ -747,6 +1236,7 @@ function renderWidgetHeader(spec, cppTypes) {
747
1236
  }
748
1237
  }
749
1238
  return `#pragma once
1239
+ #include <QByteArray>
750
1240
  #include <QDateTime>
751
1241
  #include <QHash>
752
1242
  #include <QMetaMethod>
@@ -754,6 +1244,7 @@ function renderWidgetHeader(spec, cppTypes) {
754
1244
  #include <QVariant>
755
1245
  #include <QVariantList>
756
1246
  #include <functional>
1247
+ #include <optional>
757
1248
  #include "AnQstWebHostBase.h"
758
1249
  #include "${spec.widgetName}Types.h"
759
1250
 
@@ -784,6 +1275,7 @@ ${handleMethods.join("\n")}
784
1275
  static constexpr const char* kBootstrapContentRoot = "qrc:/${spec.widgetName.toLowerCase()}";
785
1276
  static constexpr const char* kBootstrapBridgeObject = "${spec.widgetName}Bridge";
786
1277
  static constexpr int kMaxQueuedCallsPerEndpoint = 1024;
1278
+ ${dragDropHelperMethods.map((s) => ` ${s}`).join("\n")}
787
1279
 
788
1280
  handle handle;
789
1281
  ${publicMethods.map((s) => ` ${s}`).join("\n")}
@@ -806,13 +1298,6 @@ private:
806
1298
  QVariantList args;
807
1299
  QDateTime enqueuedAt;
808
1300
  };
809
- struct BridgeBindingRow {
810
- const char* service;
811
- const char* member;
812
- const char* kind;
813
- };
814
- static const BridgeBindingRow kBridgeBindings[];
815
- static constexpr int kBridgeBindingsCount = ${bindings.length};
816
1301
  static QString makeBindingKey(const QString& service, const QString& member);
817
1302
  void installBridgeBindings();
818
1303
  bool hasEmitterListeners(const QString& service, const QString& member) const;
@@ -834,15 +1319,25 @@ ${fields.map((f) => ` ${f}`).join("\n")}
834
1319
  };
835
1320
  `;
836
1321
  }
837
- function renderCppStub(spec, cppTypes) {
1322
+ function renderCppStub(spec, cppTypes, cppCodecCatalog) {
838
1323
  const widgetClassName = `${spec.widgetName}Widget`;
1324
+ const dragDropPayloadHelpers = collectDragDropPayloadHelpers(spec, cppTypes, cppCodecCatalog);
1325
+ const cppCodecHelpers = (0, boundary_codecs_1.renderCppBoundaryCodecHelpers)(cppCodecCatalog, (typeText, pathHintParts) => cppTypes.mapTypeText(typeText, pathHintParts)).trim();
839
1326
  const lines = [];
840
1327
  lines.push(`#include "include/${spec.widgetName}Widget.h"`);
1328
+ lines.push(`#include "AnQstBase93.h"`);
841
1329
  lines.push(`#include <QDebug>`);
842
1330
  lines.push(`#include <QElapsedTimer>`);
843
1331
  lines.push(`#include <QEventLoop>`);
1332
+ lines.push(`#include <QJsonArray>`);
1333
+ lines.push(`#include <QJsonDocument>`);
1334
+ lines.push(`#include <QJsonObject>`);
844
1335
  lines.push(`#include <QMetaType>`);
845
1336
  lines.push(`#include <QTimer>`);
1337
+ lines.push(`#include <cstring>`);
1338
+ lines.push(`#include <cstdint>`);
1339
+ lines.push(`#include <string>`);
1340
+ lines.push(`#include <vector>`);
846
1341
  lines.push(`#include <stdexcept>`);
847
1342
  lines.push("");
848
1343
  lines.push(`using namespace ${spec.widgetName};`);
@@ -852,7 +1347,7 @@ function renderCppStub(spec, cppTypes) {
852
1347
  lines.push("namespace {");
853
1348
  lines.push("void registerGeneratedMetaTypes() {");
854
1349
  lines.push(" static const bool registered = []() {");
855
- for (const typeName of cppTypes.structNames) {
1350
+ for (const typeName of cppTypes.metatypeNames) {
856
1351
  lines.push(` qRegisterMetaType<${spec.widgetName}::${typeName}>("${spec.widgetName}::${typeName}");`);
857
1352
  lines.push(` qRegisterMetaType<QList<${spec.widgetName}::${typeName}>>("QList<${spec.widgetName}::${typeName}>");`);
858
1353
  }
@@ -860,8 +1355,89 @@ function renderCppStub(spec, cppTypes) {
860
1355
  lines.push(" }();");
861
1356
  lines.push(" Q_UNUSED(registered);");
862
1357
  lines.push("}");
1358
+ if (cppCodecHelpers.length > 0) {
1359
+ lines.push("");
1360
+ lines.push(cppCodecHelpers);
1361
+ }
863
1362
  lines.push("}");
864
1363
  lines.push("");
1364
+ for (const helper of dragDropPayloadHelpers) {
1365
+ lines.push(`QByteArray ${widgetClassName}::encodeDragDropPayload_${helper.typeName}(const ${helper.cppType}& payload) {`);
1366
+ lines.push(` const QVariant wire = encode${helper.codecId}(payload);`);
1367
+ if (helper.carriers.includes("string")) {
1368
+ lines.push(` if (wire.type() == QVariant::String) {`);
1369
+ lines.push(` QByteArray out;`);
1370
+ lines.push(` out.append('S');`);
1371
+ lines.push(` out.append(wire.toString().toUtf8());`);
1372
+ lines.push(` return out;`);
1373
+ lines.push(` }`);
1374
+ }
1375
+ if (helper.carriers.includes("array")) {
1376
+ lines.push(` if (wire.type() == QVariant::List) {`);
1377
+ lines.push(` QByteArray out;`);
1378
+ lines.push(` out.append('A');`);
1379
+ lines.push(` out.append(QJsonDocument(QJsonArray::fromVariantList(wire.toList())).toJson(QJsonDocument::Compact));`);
1380
+ lines.push(` return out;`);
1381
+ lines.push(` }`);
1382
+ }
1383
+ if (helper.carriers.includes("object")) {
1384
+ lines.push(` if (wire.type() == QVariant::Map) {`);
1385
+ lines.push(` QByteArray out;`);
1386
+ lines.push(` out.append('O');`);
1387
+ lines.push(` out.append(QJsonDocument(QJsonObject::fromVariantMap(wire.toMap())).toJson(QJsonDocument::Compact));`);
1388
+ lines.push(` return out;`);
1389
+ lines.push(` }`);
1390
+ }
1391
+ lines.push(` throw std::runtime_error("AnQst drag/drop payload codec emitted an unsupported top-level carrier.");`);
1392
+ lines.push(`}`);
1393
+ lines.push("");
1394
+ lines.push(`std::optional<${helper.cppType}> ${widgetClassName}::decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload) {`);
1395
+ lines.push(` if (rawPayload.isEmpty()) {`);
1396
+ lines.push(` return std::nullopt;`);
1397
+ lines.push(` }`);
1398
+ lines.push(` const char transportTag = rawPayload.at(0);`);
1399
+ lines.push(` const QByteArray payloadBytes = rawPayload.mid(1);`);
1400
+ if (helper.carriers.includes("string")) {
1401
+ lines.push(` if (transportTag == 'S') {`);
1402
+ lines.push(` try {`);
1403
+ lines.push(` return decode${helper.codecId}(QString::fromUtf8(payloadBytes));`);
1404
+ lines.push(` } catch (...) {`);
1405
+ lines.push(` return std::nullopt;`);
1406
+ lines.push(` }`);
1407
+ lines.push(` }`);
1408
+ }
1409
+ if (helper.carriers.includes("array")) {
1410
+ lines.push(` if (transportTag == 'A') {`);
1411
+ lines.push(` QJsonParseError parseError;`);
1412
+ lines.push(` const QJsonDocument document = QJsonDocument::fromJson(payloadBytes, &parseError);`);
1413
+ lines.push(` if (parseError.error != QJsonParseError::NoError || !document.isArray()) {`);
1414
+ lines.push(` return std::nullopt;`);
1415
+ lines.push(` }`);
1416
+ lines.push(` try {`);
1417
+ lines.push(` return decode${helper.codecId}(QVariant(document.array().toVariantList()));`);
1418
+ lines.push(` } catch (...) {`);
1419
+ lines.push(` return std::nullopt;`);
1420
+ lines.push(` }`);
1421
+ lines.push(` }`);
1422
+ }
1423
+ if (helper.carriers.includes("object")) {
1424
+ lines.push(` if (transportTag == 'O') {`);
1425
+ lines.push(` QJsonParseError parseError;`);
1426
+ lines.push(` const QJsonDocument document = QJsonDocument::fromJson(payloadBytes, &parseError);`);
1427
+ lines.push(` if (parseError.error != QJsonParseError::NoError || !document.isObject()) {`);
1428
+ lines.push(` return std::nullopt;`);
1429
+ lines.push(` }`);
1430
+ lines.push(` try {`);
1431
+ lines.push(` return decode${helper.codecId}(QVariant(document.object().toVariantMap()));`);
1432
+ lines.push(` } catch (...) {`);
1433
+ lines.push(` return std::nullopt;`);
1434
+ lines.push(` }`);
1435
+ lines.push(` }`);
1436
+ }
1437
+ lines.push(` return std::nullopt;`);
1438
+ lines.push(`}`);
1439
+ lines.push("");
1440
+ }
865
1441
  for (const service of spec.services) {
866
1442
  for (const member of service.members) {
867
1443
  if (member.kind !== "Call" || !member.payloadTypeText)
@@ -878,14 +1454,6 @@ function renderCppStub(spec, cppTypes) {
878
1454
  lines.push("");
879
1455
  }
880
1456
  }
881
- lines.push(`const ${widgetClassName}::BridgeBindingRow ${widgetClassName}::kBridgeBindings[] = {`);
882
- for (const service of spec.services) {
883
- for (const member of service.members) {
884
- lines.push(` {"${service.name}", "${member.name}", "${member.kind}"},`);
885
- }
886
- }
887
- lines.push(`};`);
888
- lines.push("");
889
1457
  lines.push(`${widgetClassName}::${widgetClassName}(QWidget* parent) : AnQstWebHostBase(parent), handle(this) {`);
890
1458
  lines.push(` static const bool kResourcesInitialized = []() {`);
891
1459
  lines.push(` ::qInitResources_${spec.widgetName}();`);
@@ -912,17 +1480,89 @@ function renderCppStub(spec, cppTypes) {
912
1480
  for (const member of service.members) {
913
1481
  if (member.kind === "DropTarget" && member.payloadTypeText) {
914
1482
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
1483
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
1484
+ const typeName = member.payloadTypeText.replace(/\s/g, "");
915
1485
  lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_dropReceived, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
916
1486
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
917
- lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
1487
+ if (payloadSite) {
1488
+ lines.push(` if (payload.type() != QVariant::String) {`);
1489
+ lines.push(` emitHostError(`);
1490
+ lines.push(` QStringLiteral("DeserializationError"),`);
1491
+ lines.push(` QStringLiteral("bridge"),`);
1492
+ lines.push(` QStringLiteral("error"),`);
1493
+ lines.push(` true,`);
1494
+ lines.push(` QStringLiteral("Failed to deserialize DropTarget ${service.name}.${member.name}."),`);
1495
+ lines.push(` {`);
1496
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1497
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1498
+ lines.push(` {QStringLiteral("detail"), QStringLiteral("Host did not provide tagged drag/drop payload text.")},`);
1499
+ lines.push(` });`);
1500
+ lines.push(` return;`);
1501
+ lines.push(` }`);
1502
+ lines.push(` const auto decodedPayload = decodeDragDropPayload_${typeName}(payload.toString().toUtf8());`);
1503
+ lines.push(` if (!decodedPayload.has_value()) {`);
1504
+ lines.push(` emitHostError(`);
1505
+ lines.push(` QStringLiteral("DeserializationError"),`);
1506
+ lines.push(` QStringLiteral("bridge"),`);
1507
+ lines.push(` QStringLiteral("error"),`);
1508
+ lines.push(` true,`);
1509
+ lines.push(` QStringLiteral("Failed to deserialize DropTarget ${service.name}.${member.name}."),`);
1510
+ lines.push(` {`);
1511
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1512
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1513
+ lines.push(` {QStringLiteral("detail"), QStringLiteral("Tagged drag/drop payload did not match the planned boundary carrier.")},`);
1514
+ lines.push(` });`);
1515
+ lines.push(` return;`);
1516
+ lines.push(` }`);
1517
+ lines.push(` emit ${member.name}(*decodedPayload, x, y);`);
1518
+ }
1519
+ else {
1520
+ lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
1521
+ }
918
1522
  lines.push(` }`);
919
1523
  lines.push(` });`);
920
1524
  }
921
1525
  else if (member.kind === "HoverTarget" && member.payloadTypeText) {
922
1526
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
1527
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
1528
+ const typeName = member.payloadTypeText.replace(/\s/g, "");
923
1529
  lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverUpdated, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
924
1530
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
925
- lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
1531
+ if (payloadSite) {
1532
+ lines.push(` if (payload.type() != QVariant::String) {`);
1533
+ lines.push(` emitHostError(`);
1534
+ lines.push(` QStringLiteral("DeserializationError"),`);
1535
+ lines.push(` QStringLiteral("bridge"),`);
1536
+ lines.push(` QStringLiteral("error"),`);
1537
+ lines.push(` true,`);
1538
+ lines.push(` QStringLiteral("Failed to deserialize HoverTarget ${service.name}.${member.name}."),`);
1539
+ lines.push(` {`);
1540
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1541
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1542
+ lines.push(` {QStringLiteral("detail"), QStringLiteral("Host did not provide tagged drag/drop payload text.")},`);
1543
+ lines.push(` });`);
1544
+ lines.push(` return;`);
1545
+ lines.push(` }`);
1546
+ lines.push(` const auto decodedPayload = decodeDragDropPayload_${typeName}(payload.toString().toUtf8());`);
1547
+ lines.push(` if (!decodedPayload.has_value()) {`);
1548
+ lines.push(` emitHostError(`);
1549
+ lines.push(` QStringLiteral("DeserializationError"),`);
1550
+ lines.push(` QStringLiteral("bridge"),`);
1551
+ lines.push(` QStringLiteral("error"),`);
1552
+ lines.push(` true,`);
1553
+ lines.push(` QStringLiteral("Failed to deserialize HoverTarget ${service.name}.${member.name}."),`);
1554
+ lines.push(` {`);
1555
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1556
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1557
+ lines.push(` {QStringLiteral("detail"), QStringLiteral("Tagged drag/drop payload did not match the planned boundary carrier.")},`);
1558
+ lines.push(` });`);
1559
+ lines.push(` return;`);
1560
+ lines.push(` }`);
1561
+ lines.push(` emit ${member.name}(*decodedPayload, x, y);`);
1562
+ }
1563
+ else {
1564
+ lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
1565
+ }
926
1566
  lines.push(` }`);
927
1567
  lines.push(` });`);
928
1568
  lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverLeft, this, [this](const QString& service, const QString& member) {`);
@@ -1028,11 +1668,13 @@ function renderCppStub(spec, cppTypes) {
1028
1668
  continue;
1029
1669
  const timeoutMs = member.timeoutMs;
1030
1670
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
1671
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
1031
1672
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
1032
1673
  for (let i = 0; i < member.parameters.length; i++) {
1033
1674
  const p = member.parameters[i];
1034
1675
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
1035
- lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
1676
+ const paramSite = (0, boundary_codecs_1.getBoundaryParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
1677
+ lines.push(` const ${pType} ${p.name} = ${paramSite ? `decode${paramSite.codecId}(args.value(${i}))` : variantToCppExpression(pType, `args.value(${i})`)};`);
1036
1678
  }
1037
1679
  lines.push(` const QString requestId = QStringLiteral("call-%1").arg(++m_callRequestCounter);`);
1038
1680
  lines.push(` const QString queueKey = makeBindingKey(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"));`);
@@ -1049,7 +1691,7 @@ function renderCppStub(spec, cppTypes) {
1049
1691
  lines.push(` try {`);
1050
1692
  const callArgs = member.parameters.map((p) => p.name).join(", ");
1051
1693
  lines.push(` const ${cppType} result = m_${member.name}Handler(${callArgs});`);
1052
- lines.push(` return ${cppToVariantExpression(cppType, "result")};`);
1694
+ lines.push(` return ${payloadSite ? `encode${payloadSite.codecId}(result)` : cppToVariantExpression(cppType, "result")};`);
1053
1695
  lines.push(` } catch (const std::exception& ex) {`);
1054
1696
  lines.push(` return QVariantMap{`);
1055
1697
  lines.push(` {QStringLiteral("code"), QStringLiteral("CallHandlerError")},`);
@@ -1101,7 +1743,8 @@ function renderCppStub(spec, cppTypes) {
1101
1743
  for (let i = 0; i < member.parameters.length; i++) {
1102
1744
  const p = member.parameters[i];
1103
1745
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
1104
- lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
1746
+ const paramSite = (0, boundary_codecs_1.getBoundaryParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
1747
+ lines.push(` const ${pType} ${p.name} = ${paramSite ? `decode${paramSite.codecId}(args.value(${i}))` : variantToCppExpression(pType, `args.value(${i})`)};`);
1105
1748
  }
1106
1749
  const argNames = member.parameters.map((p) => p.name).join(", ");
1107
1750
  lines.push(` emit ${member.name}(${argNames});`);
@@ -1117,8 +1760,9 @@ function renderCppStub(spec, cppTypes) {
1117
1760
  if (member.kind !== "Input" || !member.payloadTypeText)
1118
1761
  continue;
1119
1762
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
1763
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
1120
1764
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
1121
- lines.push(` const ${cppType} typedValue = ${variantToCppExpression(cppType, "value")};`);
1765
+ lines.push(` const ${cppType} typedValue = ${payloadSite ? `decode${payloadSite.codecId}(value)` : variantToCppExpression(cppType, "value")};`);
1122
1766
  lines.push(` set${pascalCase(member.name)}(typedValue);`);
1123
1767
  lines.push(` if (m_${member.name}Handler) m_${member.name}Handler(typedValue);`);
1124
1768
  lines.push(` return;`);
@@ -1138,12 +1782,14 @@ function renderCppStub(spec, cppTypes) {
1138
1782
  }
1139
1783
  if (member.kind === "Slot") {
1140
1784
  const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
1785
+ const payloadSite = member.payloadTypeText ? (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name) : undefined;
1141
1786
  const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
1142
1787
  lines.push(`${ret} ${widgetClassName}::slot_${member.name}(${args}) {`);
1143
1788
  lines.push(` QVariantList invokeArgs;`);
1144
1789
  for (const p of member.parameters) {
1145
1790
  const pType = mapTsTypeToCpp(p.typeText);
1146
- lines.push(` invokeArgs.push_back(${cppToVariantExpression(pType, p.name)});`);
1791
+ const paramSite = (0, boundary_codecs_1.getBoundaryParameterSite)(cppCodecCatalog, service.name, member.name, p.name);
1792
+ lines.push(` invokeArgs.push_back(${paramSite ? `encode${paramSite.codecId}(${p.name})` : cppToVariantExpression(pType, p.name)});`);
1147
1793
  }
1148
1794
  lines.push(` QVariant result;`);
1149
1795
  lines.push(` QString invokeError;`);
@@ -1160,13 +1806,14 @@ function renderCppStub(spec, cppTypes) {
1160
1806
  lines.push(` return;`);
1161
1807
  }
1162
1808
  else {
1163
- lines.push(` return ${variantToCppExpression(ret, "result")};`);
1809
+ lines.push(` return ${payloadSite ? `decode${payloadSite.codecId}(result)` : variantToCppExpression(ret, "result")};`);
1164
1810
  }
1165
1811
  lines.push("}");
1166
1812
  lines.push("");
1167
1813
  }
1168
1814
  else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
1169
1815
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
1816
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(cppCodecCatalog, service.name, member.name);
1170
1817
  const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
1171
1818
  lines.push(`${cppType} ${widgetClassName}::${member.name}() const {`);
1172
1819
  lines.push(` return m_${member.name};`);
@@ -1174,9 +1821,40 @@ function renderCppStub(spec, cppTypes) {
1174
1821
  lines.push("");
1175
1822
  lines.push(`void ${widgetClassName}::set${cap}(const ${cppType}& value) {`);
1176
1823
  lines.push(` if (m_${member.name} == value) return;`);
1824
+ if (member.kind === "Output") {
1825
+ lines.push(` QVariant encodedValue;`);
1826
+ lines.push(` try {`);
1827
+ lines.push(` encodedValue = ${payloadSite ? `encode${payloadSite.codecId}(value)` : cppToVariantExpression(cppType, "value")};`);
1828
+ lines.push(` } catch (const std::exception& ex) {`);
1829
+ lines.push(` emitHostError(`);
1830
+ lines.push(` QStringLiteral("SerializationError"),`);
1831
+ lines.push(` QStringLiteral("bridge"),`);
1832
+ lines.push(` QStringLiteral("error"),`);
1833
+ lines.push(` true,`);
1834
+ lines.push(` QStringLiteral("Failed to serialize Output ${service.name}.${member.name}."),`);
1835
+ lines.push(` {`);
1836
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1837
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1838
+ lines.push(` {QStringLiteral("detail"), QString::fromUtf8(ex.what())},`);
1839
+ lines.push(` });`);
1840
+ lines.push(` return;`);
1841
+ lines.push(` } catch (...) {`);
1842
+ lines.push(` emitHostError(`);
1843
+ lines.push(` QStringLiteral("SerializationError"),`);
1844
+ lines.push(` QStringLiteral("bridge"),`);
1845
+ lines.push(` QStringLiteral("error"),`);
1846
+ lines.push(` true,`);
1847
+ lines.push(` QStringLiteral("Failed to serialize Output ${service.name}.${member.name}."),`);
1848
+ lines.push(` {`);
1849
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1850
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1851
+ lines.push(` });`);
1852
+ lines.push(` return;`);
1853
+ lines.push(` }`);
1854
+ }
1177
1855
  lines.push(` m_${member.name} = value;`);
1178
1856
  if (member.kind === "Output") {
1179
- lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), ${cppToVariantExpression(cppType, "value")});`);
1857
+ lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), encodedValue);`);
1180
1858
  }
1181
1859
  lines.push(` emit ${member.name}Changed(value);`);
1182
1860
  lines.push("}");
@@ -1363,59 +2041,144 @@ function slotHandlerReturnType(tsRet) {
1363
2041
  }
1364
2042
  return `${tsRet} | Promise<${tsRet}> | Error`;
1365
2043
  }
1366
- function renderTsService(spec, serviceName) {
2044
+ function renderTsService(spec, serviceName, codecCatalog) {
1367
2045
  const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
1368
2046
  const fieldLines = [];
1369
2047
  const methodLines = [];
1370
2048
  const setMembers = [];
1371
2049
  const onSlotMembers = [];
1372
2050
  const constructorBodyLines = [];
1373
- constructorBodyLines.push(" this._bridge.ready().catch((error) => console.error('AnQst bridge ready() failed', error, (error as { stack?: unknown })?.stack));");
1374
2051
  for (const m of members) {
1375
2052
  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}]` : "[]";
2053
+ const paramSites = m.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, serviceName, m.name, p.name));
2054
+ const encodedValueArray = paramSites.length > 0
2055
+ ? `[${m.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
2056
+ : "[]";
2057
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, serviceName, m.name);
1378
2058
  if (m.kind === "Call") {
1379
2059
  const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
1380
- methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${valueArray}); }`);
2060
+ if (payloadSite) {
2061
+ methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { const result = await this._bridge.call<unknown>("${serviceName}", "${m.name}", ${encodedValueArray}); return decode${payloadSite.codecId}(result); }`);
2062
+ }
2063
+ else {
2064
+ methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${encodedValueArray}); }`);
2065
+ }
1381
2066
  continue;
1382
2067
  }
1383
2068
  if (m.kind === "Emitter") {
1384
- methodLines.push(` ${m.name}(${args}): void { this._bridge.emit("${serviceName}", "${m.name}", ${valueArray}); }`);
2069
+ methodLines.push(` ${m.name}(${args}): void {`);
2070
+ methodLines.push(` let encodedArgs: unknown[];`);
2071
+ methodLines.push(` try {`);
2072
+ methodLines.push(` encodedArgs = ${encodedValueArray};`);
2073
+ methodLines.push(` } catch (error) {`);
2074
+ methodLines.push(` this._bridge.reportFrontendDiagnostic({`);
2075
+ methodLines.push(` code: "SerializationError",`);
2076
+ methodLines.push(` severity: "error",`);
2077
+ methodLines.push(` category: "bridge",`);
2078
+ methodLines.push(` recoverable: true,`);
2079
+ methodLines.push(` message: \`Failed to serialize Emitter ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
2080
+ methodLines.push(` service: "${serviceName}",`);
2081
+ methodLines.push(` member: "${m.name}",`);
2082
+ methodLines.push(` context: { interaction: "Emitter" }`);
2083
+ methodLines.push(` });`);
2084
+ methodLines.push(` return;`);
2085
+ methodLines.push(` }`);
2086
+ methodLines.push(` this._bridge.emit("${serviceName}", "${m.name}", encodedArgs);`);
2087
+ methodLines.push(` }`);
1385
2088
  continue;
1386
2089
  }
1387
2090
  if (m.kind === "Slot") {
1388
2091
  const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
2092
+ const decodedArgs = m.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(wireArgs[${index}])` : `wireArgs[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ");
1389
2093
  onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
1390
- onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", handler as (...args: unknown[]) => unknown);`);
2094
+ onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", (...wireArgs: unknown[]) => {`);
2095
+ onSlotMembers.push(` const result = handler(${decodedArgs});`);
2096
+ if (payloadSite) {
2097
+ onSlotMembers.push(` if (result instanceof Promise) return result.then((value) => value instanceof Error ? value : encode${payloadSite.codecId}(value));`);
2098
+ onSlotMembers.push(` return result instanceof Error ? result : encode${payloadSite.codecId}(result);`);
2099
+ }
2100
+ else {
2101
+ onSlotMembers.push(" return result;");
2102
+ }
2103
+ onSlotMembers.push(" });");
1391
2104
  onSlotMembers.push(" },");
1392
2105
  continue;
1393
2106
  }
1394
2107
  if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
1395
2108
  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}(); }`);
2109
+ fieldLines.push(` private readonly _${m.name} = signal<${tsType} | undefined>(undefined);`);
2110
+ methodLines.push(` ${m.name}(): ${tsType} | undefined { return this._${m.name}(); }`);
1398
2111
  if (m.kind === "Input") {
1399
2112
  setMembers.push(` ${m.name}: (value: ${tsType}): void => {`);
2113
+ setMembers.push(` let encodedValue: unknown;`);
2114
+ setMembers.push(` try {`);
2115
+ setMembers.push(` encodedValue = ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"};`);
2116
+ setMembers.push(` } catch (error) {`);
2117
+ setMembers.push(` this._bridge.reportFrontendDiagnostic({`);
2118
+ setMembers.push(` code: "SerializationError",`);
2119
+ setMembers.push(` severity: "error",`);
2120
+ setMembers.push(` category: "bridge",`);
2121
+ setMembers.push(` recoverable: true,`);
2122
+ setMembers.push(` message: \`Failed to serialize Input ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
2123
+ setMembers.push(` service: "${serviceName}",`);
2124
+ setMembers.push(` member: "${m.name}",`);
2125
+ setMembers.push(` context: { interaction: "Input" }`);
2126
+ setMembers.push(` });`);
2127
+ setMembers.push(` return;`);
2128
+ setMembers.push(` }`);
1400
2129
  setMembers.push(` this._${m.name}.set(value);`);
1401
- setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", value);`);
2130
+ setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", encodedValue);`);
1402
2131
  setMembers.push(" },");
1403
2132
  }
1404
2133
  if (m.kind === "Output") {
1405
- constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => this._${m.name}.set(value as ${tsType}));`);
2134
+ constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => {`);
2135
+ constructorBodyLines.push(` this._${m.name}.set(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${tsType}`});`);
2136
+ constructorBodyLines.push(` });`);
1406
2137
  }
1407
2138
  }
1408
2139
  if (m.kind === "DropTarget" && m.payloadTypeText) {
1409
2140
  const tsType = mapTypeTextToTs(m.payloadTypeText);
2141
+ const typeName = m.payloadTypeText.replace(/\s/g, "");
1410
2142
  fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
1411
2143
  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 }));`);
2144
+ constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) => {`);
2145
+ constructorBodyLines.push(` try {`);
2146
+ constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
2147
+ constructorBodyLines.push(` } catch (error) {`);
2148
+ constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
2149
+ constructorBodyLines.push(` code: "DeserializationError",`);
2150
+ constructorBodyLines.push(` severity: "error",`);
2151
+ constructorBodyLines.push(` category: "bridge",`);
2152
+ constructorBodyLines.push(` recoverable: true,`);
2153
+ constructorBodyLines.push(` message: \`Failed to deserialize DropTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
2154
+ constructorBodyLines.push(` service: "${serviceName}",`);
2155
+ constructorBodyLines.push(` member: "${m.name}",`);
2156
+ constructorBodyLines.push(` context: { interaction: "DropTarget" }`);
2157
+ constructorBodyLines.push(` });`);
2158
+ constructorBodyLines.push(` }`);
2159
+ constructorBodyLines.push(` });`);
1413
2160
  }
1414
2161
  if (m.kind === "HoverTarget" && m.payloadTypeText) {
1415
2162
  const tsType = mapTypeTextToTs(m.payloadTypeText);
2163
+ const typeName = m.payloadTypeText.replace(/\s/g, "");
1416
2164
  fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
1417
2165
  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 }));`);
2166
+ constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) => {`);
2167
+ constructorBodyLines.push(` try {`);
2168
+ constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
2169
+ constructorBodyLines.push(` } catch (error) {`);
2170
+ constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
2171
+ constructorBodyLines.push(` code: "DeserializationError",`);
2172
+ constructorBodyLines.push(` severity: "error",`);
2173
+ constructorBodyLines.push(` category: "bridge",`);
2174
+ constructorBodyLines.push(` recoverable: true,`);
2175
+ constructorBodyLines.push(` message: \`Failed to deserialize HoverTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
2176
+ constructorBodyLines.push(` service: "${serviceName}",`);
2177
+ constructorBodyLines.push(` member: "${m.name}",`);
2178
+ constructorBodyLines.push(` context: { interaction: "HoverTarget" }`);
2179
+ constructorBodyLines.push(` });`);
2180
+ constructorBodyLines.push(` }`);
2181
+ constructorBodyLines.push(` });`);
1419
2182
  constructorBodyLines.push(` this._bridge.onHoverLeft("${serviceName}", "${m.name}", () => this._${m.name}.set(null));`);
1420
2183
  }
1421
2184
  }
@@ -1464,7 +2227,7 @@ function renderTsServiceDts(spec, serviceName) {
1464
2227
  }
1465
2228
  if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
1466
2229
  const tsType = mapTypeTextToTs(m.payloadTypeText);
1467
- classMembers.push(` ${m.name}(): ${tsType};`);
2230
+ classMembers.push(` ${m.name}(): ${tsType} | undefined;`);
1468
2231
  if (m.kind === "Input") {
1469
2232
  setMembers.push(` ${m.name}(value: ${tsType}): void;`);
1470
2233
  }
@@ -1494,15 +2257,21 @@ export declare class ${serviceName} {
1494
2257
  readonly onSlot: ${onSlotInterfaceName};${classMemberBlock}
1495
2258
  }`;
1496
2259
  }
1497
- function renderTsServices(spec) {
1498
- const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name)).join("\n");
2260
+ function renderTsServices(spec, codecCatalog) {
2261
+ const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name, codecCatalog)).join("\n");
1499
2262
  const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/services.ts`).trim();
1500
2263
  const localTypeImports = renderLocalTypeImports(spec).trim();
1501
2264
  const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
1502
2265
  const typeImportsBlock = typeImports.length > 0 ? `${typeImports}\n\n` : "";
2266
+ const dragDropHelperBlock = renderTsDragDropPayloadHelpers(spec, codecCatalog).trim();
2267
+ const dragDropHelpers = dragDropHelperBlock.length > 0 ? `\n// Drag/drop payload helpers\n${dragDropHelperBlock}\n` : "";
1503
2268
  return `import { Injectable, inject, signal } from "@angular/core";
1504
2269
  ${typeImportsBlock}
1505
2270
 
2271
+ // Boundary codec plan helpers
2272
+ ${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
2273
+ ${dragDropHelpers}
2274
+
1506
2275
  type SlotHandler = (...args: unknown[]) => unknown;
1507
2276
  type OutputHandler = (value: unknown) => void;
1508
2277
  type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
@@ -1510,6 +2279,28 @@ type OutputListener = (service: string, member: string, value: unknown) => void;
1510
2279
  type DropListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
1511
2280
  type HoverListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
1512
2281
  type HoverLeftListener = (service: string, member: string) => void;
2282
+ type HostDiagnosticListener = (payload: unknown) => void;
2283
+ type DisconnectListener = () => void;
2284
+
2285
+ export type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
2286
+ export type AnQstBridgeSource = "frontend" | "host";
2287
+ export type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
2288
+ export type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
2289
+
2290
+ export interface AnQstBridgeDiagnostic {
2291
+ code: string;
2292
+ severity: AnQstBridgeSeverity;
2293
+ category: string;
2294
+ recoverable: boolean;
2295
+ message: string;
2296
+ timestamp: string;
2297
+ source: AnQstBridgeSource;
2298
+ transport?: AnQstBridgeTransport;
2299
+ service?: string;
2300
+ member?: string;
2301
+ requestId?: string;
2302
+ context?: Record<string, unknown>;
2303
+ }
1513
2304
 
1514
2305
  interface HostBridgeApi {
1515
2306
  anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
@@ -1521,6 +2312,7 @@ interface HostBridgeApi {
1521
2312
  anQstBridge_slotInvocationRequested: {
1522
2313
  connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
1523
2314
  };
2315
+ anQstBridge_hostDiagnostic?: { connect: (cb: (payload: unknown) => void) => void };
1524
2316
  anQstBridge_dropReceived: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
1525
2317
  anQstBridge_hoverUpdated: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
1526
2318
  anQstBridge_hoverLeft: { connect: (cb: (service: string, member: string) => void) => void };
@@ -1534,6 +2326,7 @@ interface QWebChannelCtor {
1534
2326
  }
1535
2327
 
1536
2328
  interface BridgeAdapter {
2329
+ readonly transport: AnQstBridgeTransport;
1537
2330
  call<T>(service: string, member: string, args: unknown[]): Promise<T>;
1538
2331
  emit(service: string, member: string, args: unknown[]): void;
1539
2332
  setInput(service: string, member: string, value: unknown): void;
@@ -1541,11 +2334,82 @@ interface BridgeAdapter {
1541
2334
  resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
1542
2335
  onOutput(handler: OutputListener): void;
1543
2336
  onSlotInvocation(handler: SlotInvocationListener): void;
2337
+ onHostDiagnostic(handler: HostDiagnosticListener): void;
2338
+ onDisconnected(handler: DisconnectListener): void;
1544
2339
  onDrop(handler: DropListener): void;
1545
2340
  onHover(handler: HoverListener): void;
1546
2341
  onHoverLeft(handler: HoverLeftListener): void;
1547
2342
  }
1548
2343
 
2344
+ function errorMessage(error: unknown): string {
2345
+ if (error instanceof Error && typeof error.message === "string" && error.message.length > 0) {
2346
+ return error.message;
2347
+ }
2348
+ return String(error);
2349
+ }
2350
+
2351
+ function normalizeSeverity(value: unknown): AnQstBridgeSeverity {
2352
+ if (value === "info" || value === "warn" || value === "error" || value === "fatal") {
2353
+ return value;
2354
+ }
2355
+ return "error";
2356
+ }
2357
+
2358
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
2359
+ if (value === null || typeof value !== "object") {
2360
+ return undefined;
2361
+ }
2362
+ return value as Record<string, unknown>;
2363
+ }
2364
+
2365
+ function readString(record: Record<string, unknown> | undefined, key: string): string | undefined {
2366
+ const value = record?.[key];
2367
+ return typeof value === "string" && value.length > 0 ? value : undefined;
2368
+ }
2369
+
2370
+ function readBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
2371
+ const value = record?.[key];
2372
+ return typeof value === "boolean" ? value : undefined;
2373
+ }
2374
+
2375
+ function readContext(record: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
2376
+ const context = asRecord(record?.["context"]);
2377
+ return context === undefined ? undefined : context;
2378
+ }
2379
+
2380
+ function normalizeHostDiagnostic(
2381
+ payload: unknown,
2382
+ transport: AnQstBridgeTransport
2383
+ ): Omit<AnQstBridgeDiagnostic, "timestamp"> {
2384
+ const row = asRecord(payload);
2385
+ if (row === undefined) {
2386
+ return {
2387
+ code: "HostDiagnosticMalformed",
2388
+ severity: "error",
2389
+ category: "bridge",
2390
+ recoverable: true,
2391
+ message: "Host emitted a malformed diagnostic payload.",
2392
+ source: "host",
2393
+ transport
2394
+ };
2395
+ }
2396
+
2397
+ const context = readContext(row);
2398
+ return {
2399
+ code: readString(row, "code") ?? "HostDiagnostic",
2400
+ severity: normalizeSeverity(row["severity"]),
2401
+ category: readString(row, "category") ?? "bridge",
2402
+ recoverable: readBoolean(row, "recoverable") ?? true,
2403
+ message: readString(row, "message") ?? "Host emitted a diagnostic payload.",
2404
+ source: "host",
2405
+ transport,
2406
+ service: readString(row, "service") ?? readString(context, "service"),
2407
+ member: readString(row, "member") ?? readString(context, "member"),
2408
+ requestId: readString(row, "requestId") ?? readString(context, "requestId"),
2409
+ context
2410
+ };
2411
+ }
2412
+
1549
2413
  function isBridgeCallError(value: unknown): value is {
1550
2414
  code: unknown;
1551
2415
  message: unknown;
@@ -1565,6 +2429,8 @@ function isBridgeCallError(value: unknown): value is {
1565
2429
  }
1566
2430
 
1567
2431
  class QtWebChannelAdapter implements BridgeAdapter {
2432
+ readonly transport = "qt-webchannel" as const;
2433
+
1568
2434
  private constructor(private readonly host: HostBridgeApi) {}
1569
2435
 
1570
2436
  static async create(): Promise<QtWebChannelAdapter> {
@@ -1632,6 +2498,14 @@ class QtWebChannelAdapter implements BridgeAdapter {
1632
2498
  this.host.anQstBridge_slotInvocationRequested.connect(handler);
1633
2499
  }
1634
2500
 
2501
+ onHostDiagnostic(handler: HostDiagnosticListener): void {
2502
+ this.host.anQstBridge_hostDiagnostic?.connect(handler);
2503
+ }
2504
+
2505
+ onDisconnected(_handler: DisconnectListener): void {
2506
+ // QWebChannel does not expose a deterministic disconnect event here.
2507
+ }
2508
+
1635
2509
  onDrop(handler: DropListener): void {
1636
2510
  this.host.anQstBridge_dropReceived.connect(handler);
1637
2511
  }
@@ -1646,6 +2520,7 @@ class QtWebChannelAdapter implements BridgeAdapter {
1646
2520
  }
1647
2521
 
1648
2522
  class WebSocketBridgeAdapter implements BridgeAdapter {
2523
+ readonly transport = "dev-websocket" as const;
1649
2524
  private readonly pending = new Map<string, {
1650
2525
  service: string;
1651
2526
  member: string;
@@ -1655,6 +2530,8 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1655
2530
  }>();
1656
2531
  private readonly outputListeners: OutputListener[] = [];
1657
2532
  private readonly slotListeners: SlotInvocationListener[] = [];
2533
+ private readonly hostDiagnosticListeners: HostDiagnosticListener[] = [];
2534
+ private readonly disconnectListeners: DisconnectListener[] = [];
1658
2535
  private readonly dropListeners: DropListener[] = [];
1659
2536
  private readonly hoverListeners: HoverListener[] = [];
1660
2537
  private readonly hoverLeftListeners: HoverLeftListener[] = [];
@@ -1726,7 +2603,9 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1726
2603
  return;
1727
2604
  }
1728
2605
  if (type === "hostError") {
1729
- console.error("AnQst host error:", message["payload"]);
2606
+ for (const listener of this.hostDiagnosticListeners) {
2607
+ listener(message["payload"]);
2608
+ }
1730
2609
  return;
1731
2610
  }
1732
2611
  if (type === "widgetReattached") {
@@ -1745,6 +2624,9 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1745
2624
  });
1746
2625
  }
1747
2626
  this.pending.clear();
2627
+ for (const listener of this.disconnectListeners) {
2628
+ listener();
2629
+ }
1748
2630
  });
1749
2631
  }
1750
2632
 
@@ -1813,6 +2695,14 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1813
2695
  this.slotListeners.push(handler);
1814
2696
  }
1815
2697
 
2698
+ onHostDiagnostic(handler: HostDiagnosticListener): void {
2699
+ this.hostDiagnosticListeners.push(handler);
2700
+ }
2701
+
2702
+ onDisconnected(handler: DisconnectListener): void {
2703
+ this.disconnectListeners.push(handler);
2704
+ }
2705
+
1816
2706
  onDrop(handler: DropListener): void {
1817
2707
  this.dropListeners.push(handler);
1818
2708
  }
@@ -1828,53 +2718,116 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1828
2718
 
1829
2719
  @Injectable({ providedIn: "root" })
1830
2720
  class AnQstBridgeRuntime {
2721
+ private static readonly maxDiagnostics = 50;
1831
2722
  private adapter: BridgeAdapter | null = null;
1832
2723
  private readonly slotHandlers = new Map<string, SlotHandler>();
1833
2724
  private readonly outputHandlers = new Map<string, OutputHandler[]>();
1834
2725
  private readonly dropHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
1835
2726
  private readonly hoverHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
1836
2727
  private readonly hoverLeftHandlers = new Map<string, (() => void)[]>();
1837
- private readonly startup = this.init();
2728
+ private readonly diagnosticListeners = new Set<(diagnostic: AnQstBridgeDiagnostic) => void>();
2729
+ private readonly _diagnostics = signal<readonly AnQstBridgeDiagnostic[]>([]);
2730
+ private readonly _state = signal<AnQstBridgeState>("starting");
2731
+ private readonly startup = this.init().catch((error) => {
2732
+ this._state.set("failed");
2733
+ this.reportFrontendDiagnostic({
2734
+ code: "BridgeBootstrapError",
2735
+ severity: "fatal",
2736
+ category: "bridge",
2737
+ recoverable: false,
2738
+ message: \`Failed to initialize bridge: \${errorMessage(error)}\`
2739
+ });
2740
+ throw error;
2741
+ });
2742
+
2743
+ diagnostics(): readonly AnQstBridgeDiagnostic[] {
2744
+ return this._diagnostics();
2745
+ }
2746
+
2747
+ state(): AnQstBridgeState {
2748
+ return this._state();
2749
+ }
2750
+
2751
+ subscribeDiagnostics(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
2752
+ this.diagnosticListeners.add(listener);
2753
+ return () => this.diagnosticListeners.delete(listener);
2754
+ }
1838
2755
 
1839
2756
  async ready(): Promise<void> {
1840
2757
  return this.startup;
1841
2758
  }
1842
2759
 
2760
+ reportFrontendDiagnostic(diagnostic: Omit<AnQstBridgeDiagnostic, "timestamp" | "source">): void {
2761
+ this.pushDiagnostic({
2762
+ ...diagnostic,
2763
+ source: "frontend",
2764
+ transport: diagnostic.transport ?? this.adapter?.transport,
2765
+ timestamp: new Date().toISOString()
2766
+ });
2767
+ }
2768
+
1843
2769
  async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
1844
2770
  const adapter = await this.requireAdapter();
1845
2771
  return adapter.call<T>(service, member, args);
1846
2772
  }
1847
2773
 
1848
2774
  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));
2775
+ this.publishNonCall("Emitter", service, member, (adapter) => adapter.emit(service, member, args));
1856
2776
  }
1857
2777
 
1858
2778
  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));
2779
+ this.publishNonCall("Input", service, member, (adapter) => adapter.setInput(service, member, value));
1866
2780
  }
1867
2781
 
1868
2782
  registerSlot(service: string, member: string, handler: SlotHandler): void {
1869
2783
  const key = this.key(service, member);
1870
2784
  this.slotHandlers.set(key, handler);
1871
2785
  if (this.adapter !== null) {
1872
- this.adapter.registerSlot(service, member);
2786
+ try {
2787
+ this.adapter.registerSlot(service, member);
2788
+ } catch (error) {
2789
+ this.reportFrontendDiagnostic({
2790
+ code: "BridgePublishError",
2791
+ severity: "error",
2792
+ category: "bridge",
2793
+ recoverable: true,
2794
+ message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
2795
+ service,
2796
+ member,
2797
+ context: { interaction: "Slot" }
2798
+ });
2799
+ }
1873
2800
  return;
1874
2801
  }
1875
2802
  this.ready()
1876
- .then(() => this.requireAdapterSync().registerSlot(service, member))
1877
- .catch((error) => console.error(error));
2803
+ .then(() => {
2804
+ try {
2805
+ this.requireAdapterSync().registerSlot(service, member);
2806
+ } catch (error) {
2807
+ this.reportFrontendDiagnostic({
2808
+ code: "BridgePublishError",
2809
+ severity: "error",
2810
+ category: "bridge",
2811
+ recoverable: true,
2812
+ message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
2813
+ service,
2814
+ member,
2815
+ context: { interaction: "Slot" }
2816
+ });
2817
+ }
2818
+ })
2819
+ .catch((error) => {
2820
+ this.reportFrontendDiagnostic({
2821
+ code: "BridgePublishError",
2822
+ severity: "error",
2823
+ category: "bridge",
2824
+ recoverable: true,
2825
+ message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
2826
+ service,
2827
+ member,
2828
+ context: { interaction: "Slot" }
2829
+ });
2830
+ });
1878
2831
  }
1879
2832
 
1880
2833
  onOutput(service: string, member: string, handler: OutputHandler): void {
@@ -1917,6 +2870,73 @@ class AnQstBridgeRuntime {
1917
2870
  return this.requireAdapterSync();
1918
2871
  }
1919
2872
 
2873
+ private pushDiagnostic(diagnostic: AnQstBridgeDiagnostic): void {
2874
+ const previous = this._diagnostics();
2875
+ const trimmed = previous.length >= AnQstBridgeRuntime.maxDiagnostics
2876
+ ? previous.slice(previous.length - (AnQstBridgeRuntime.maxDiagnostics - 1))
2877
+ : previous;
2878
+ const next = [...trimmed, diagnostic];
2879
+ this._diagnostics.set(next);
2880
+ for (const listener of this.diagnosticListeners) {
2881
+ listener(diagnostic);
2882
+ }
2883
+ }
2884
+
2885
+ private publishNonCall(
2886
+ interaction: "Emitter" | "Input",
2887
+ service: string,
2888
+ member: string,
2889
+ publish: (adapter: BridgeAdapter) => void
2890
+ ): void {
2891
+ if (this.adapter !== null) {
2892
+ try {
2893
+ publish(this.adapter);
2894
+ } catch (error) {
2895
+ this.reportFrontendDiagnostic({
2896
+ code: "BridgePublishError",
2897
+ severity: "error",
2898
+ category: "bridge",
2899
+ recoverable: true,
2900
+ message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
2901
+ service,
2902
+ member,
2903
+ context: { interaction }
2904
+ });
2905
+ }
2906
+ return;
2907
+ }
2908
+
2909
+ this.ready()
2910
+ .then(() => {
2911
+ try {
2912
+ publish(this.requireAdapterSync());
2913
+ } catch (error) {
2914
+ this.reportFrontendDiagnostic({
2915
+ code: "BridgePublishError",
2916
+ severity: "error",
2917
+ category: "bridge",
2918
+ recoverable: true,
2919
+ message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
2920
+ service,
2921
+ member,
2922
+ context: { interaction }
2923
+ });
2924
+ }
2925
+ })
2926
+ .catch((error) => {
2927
+ this.reportFrontendDiagnostic({
2928
+ code: "BridgePublishError",
2929
+ severity: "error",
2930
+ category: "bridge",
2931
+ recoverable: true,
2932
+ message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
2933
+ service,
2934
+ member,
2935
+ context: { interaction }
2936
+ });
2937
+ });
2938
+ }
2939
+
1920
2940
  private async init(): Promise<void> {
1921
2941
  const anyWindow = window as unknown as { qt?: { webChannelTransport?: unknown }; QWebChannel?: QWebChannelCtor };
1922
2942
  if (typeof anyWindow.QWebChannel === "function" && anyWindow.qt?.webChannelTransport !== undefined) {
@@ -1925,44 +2945,98 @@ class AnQstBridgeRuntime {
1925
2945
  this.adapter = await WebSocketBridgeAdapter.create();
1926
2946
  }
1927
2947
 
1928
- this.adapter.onOutput((service, member, value) => {
2948
+ const adapter = this.adapter;
2949
+ adapter.onHostDiagnostic((payload) => {
2950
+ this.pushDiagnostic({
2951
+ ...normalizeHostDiagnostic(payload, adapter.transport),
2952
+ timestamp: new Date().toISOString()
2953
+ });
2954
+ });
2955
+ adapter.onDisconnected(() => {
2956
+ this._state.set("disconnected");
2957
+ this.reportFrontendDiagnostic({
2958
+ code: "BridgeDisconnectedError",
2959
+ severity: "error",
2960
+ category: "bridge",
2961
+ recoverable: true,
2962
+ message: "Bridge disconnected.",
2963
+ transport: adapter.transport
2964
+ });
2965
+ });
2966
+
2967
+ adapter.onOutput((service, member, value) => {
1929
2968
  const key = this.key(service, member);
1930
2969
  for (const outputHandler of this.outputHandlers.get(key) ?? []) {
1931
2970
  outputHandler(value);
1932
2971
  }
1933
2972
  });
1934
- this.adapter.onSlotInvocation(async (requestId, service, member, args) => {
2973
+ adapter.onSlotInvocation(async (requestId, service, member, args) => {
1935
2974
  const key = this.key(service, member);
1936
2975
  const handler = this.slotHandlers.get(key);
1937
2976
  if (handler === undefined) {
1938
- this.adapter!.resolveSlot(requestId, false, undefined, "No slot handler registered.");
2977
+ this.reportFrontendDiagnostic({
2978
+ code: "HandlerNotRegisteredError",
2979
+ severity: "error",
2980
+ category: "bridge",
2981
+ recoverable: true,
2982
+ message: \`No slot handler registered for \${service}.\${member}.\`,
2983
+ service,
2984
+ member,
2985
+ requestId,
2986
+ context: { interaction: "Slot" }
2987
+ });
2988
+ adapter.resolveSlot(requestId, false, undefined, "No slot handler registered.");
1939
2989
  return;
1940
2990
  }
1941
2991
  try {
1942
2992
  const result = await Promise.resolve(handler(...args));
1943
2993
  if (result instanceof Error) {
1944
- this.adapter!.resolveSlot(requestId, false, undefined, result.message);
2994
+ this.reportFrontendDiagnostic({
2995
+ code: "SlotRequestFailed",
2996
+ severity: "error",
2997
+ category: "bridge",
2998
+ recoverable: true,
2999
+ message: result.message.length > 0
3000
+ ? result.message
3001
+ : \`Slot \${service}.\${member} returned an Error.\`,
3002
+ service,
3003
+ member,
3004
+ requestId,
3005
+ context: { interaction: "Slot" }
3006
+ });
3007
+ adapter.resolveSlot(requestId, false, undefined, result.message);
1945
3008
  return;
1946
3009
  }
1947
- this.adapter!.resolveSlot(requestId, true, result, "");
3010
+ adapter.resolveSlot(requestId, true, result, "");
1948
3011
  } catch (error) {
1949
- const message = error instanceof Error ? error.message : String(error);
1950
- this.adapter!.resolveSlot(requestId, false, undefined, message);
3012
+ const message = errorMessage(error);
3013
+ this.reportFrontendDiagnostic({
3014
+ code: "SlotHandlerError",
3015
+ severity: "error",
3016
+ category: "bridge",
3017
+ recoverable: true,
3018
+ message: \`Slot handler \${service}.\${member} threw: \${message}\`,
3019
+ service,
3020
+ member,
3021
+ requestId,
3022
+ context: { interaction: "Slot" }
3023
+ });
3024
+ adapter.resolveSlot(requestId, false, undefined, message);
1951
3025
  }
1952
3026
  });
1953
- this.adapter.onDrop((service, member, payload, x, y) => {
3027
+ adapter.onDrop((service, member, payload, x, y) => {
1954
3028
  const key = this.key(service, member);
1955
3029
  for (const handler of this.dropHandlers.get(key) ?? []) {
1956
3030
  handler(payload, x, y);
1957
3031
  }
1958
3032
  });
1959
- this.adapter.onHover((service, member, payload, x, y) => {
3033
+ adapter.onHover((service, member, payload, x, y) => {
1960
3034
  const key = this.key(service, member);
1961
3035
  for (const handler of this.hoverHandlers.get(key) ?? []) {
1962
3036
  handler(payload, x, y);
1963
3037
  }
1964
3038
  });
1965
- this.adapter.onHoverLeft((service, member) => {
3039
+ adapter.onHoverLeft((service, member) => {
1966
3040
  const key = this.key(service, member);
1967
3041
  for (const handler of this.hoverLeftHandlers.get(key) ?? []) {
1968
3042
  handler();
@@ -1971,9 +3045,10 @@ class AnQstBridgeRuntime {
1971
3045
  for (const key of this.slotHandlers.keys()) {
1972
3046
  const parts = key.split("::");
1973
3047
  if (parts.length === 2) {
1974
- this.adapter.registerSlot(parts[0], parts[1]);
3048
+ adapter.registerSlot(parts[0], parts[1]);
1975
3049
  }
1976
3050
  }
3051
+ this._state.set("ready");
1977
3052
  }
1978
3053
 
1979
3054
  private key(service: string, member: string): string {
@@ -1981,6 +3056,23 @@ class AnQstBridgeRuntime {
1981
3056
  }
1982
3057
 
1983
3058
  }
3059
+
3060
+ @Injectable({ providedIn: "root" })
3061
+ export class AnQstBridgeDiagnostics {
3062
+ private readonly _bridge = inject(AnQstBridgeRuntime);
3063
+
3064
+ diagnostics(): readonly AnQstBridgeDiagnostic[] {
3065
+ return this._bridge.diagnostics();
3066
+ }
3067
+
3068
+ state(): AnQstBridgeState {
3069
+ return this._bridge.state();
3070
+ }
3071
+
3072
+ subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
3073
+ return this._bridge.subscribeDiagnostics(listener);
3074
+ }
3075
+ }
1984
3076
  ${serviceClasses}
1985
3077
  `;
1986
3078
  }
@@ -1993,10 +3085,38 @@ function renderTsTypes(spec) {
1993
3085
  function renderTypeServicesDts(spec) {
1994
3086
  const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/services.d.ts`).trim();
1995
3087
  const localTypeImports = renderLocalTypeImports(spec).trim();
3088
+ const bridgeDiagnosticsDecl = `export type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
3089
+
3090
+ export type AnQstBridgeSource = "frontend" | "host";
3091
+
3092
+ export type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
3093
+
3094
+ export type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
3095
+
3096
+ export interface AnQstBridgeDiagnostic {
3097
+ code: string;
3098
+ severity: AnQstBridgeSeverity;
3099
+ category: string;
3100
+ recoverable: boolean;
3101
+ message: string;
3102
+ timestamp: string;
3103
+ source: AnQstBridgeSource;
3104
+ transport?: AnQstBridgeTransport;
3105
+ service?: string;
3106
+ member?: string;
3107
+ requestId?: string;
3108
+ context?: Record<string, unknown>;
3109
+ }
3110
+
3111
+ export declare class AnQstBridgeDiagnostics {
3112
+ diagnostics(): readonly AnQstBridgeDiagnostic[];
3113
+ state(): AnQstBridgeState;
3114
+ subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void;
3115
+ }`;
1996
3116
  const serviceDecls = spec.services
1997
3117
  .map((s) => renderTsServiceDts(spec, s.name))
1998
3118
  .join("\n\n");
1999
- const sections = [externalTypeImports, localTypeImports, serviceDecls.trim()].filter((s) => s.length > 0);
3119
+ const sections = [externalTypeImports, localTypeImports, bridgeDiagnosticsDecl, serviceDecls.trim()].filter((s) => s.length > 0);
2000
3120
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
2001
3121
  }
2002
3122
  function renderTypeTypesDts(spec) {
@@ -2071,7 +3191,7 @@ function renderNodeExpressWsTypes(spec) {
2071
3191
  const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
2072
3192
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
2073
3193
  }
2074
- function renderNodeExpressWsIndex(spec) {
3194
+ function renderNodeExpressWsIndex(spec, codecCatalog) {
2075
3195
  const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
2076
3196
  const typeDecls = renderTypeDeclarations(spec, true);
2077
3197
  const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
@@ -2103,8 +3223,13 @@ function renderNodeExpressWsIndex(spec) {
2103
3223
  .map((member) => {
2104
3224
  const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
2105
3225
  const args = nodeParamArgs(member);
3226
+ const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
3227
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
3228
+ const encodedArgs = member.parameters.length > 0
3229
+ ? `[${member.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
3230
+ : "[]";
2106
3231
  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}>;
3232
+ return this.invokeSlot("${service.name}", "${member.name}", ${encodedArgs}, timeoutMs).then((value) => ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${ret}`});
2108
3233
  }`;
2109
3234
  }))
2110
3235
  .join("\n");
@@ -2113,8 +3238,9 @@ function renderNodeExpressWsIndex(spec) {
2113
3238
  .filter((member) => member.kind === "Output" && member.payloadTypeText)
2114
3239
  .map((member) => {
2115
3240
  const typeText = mapTypeTextToTs(member.payloadTypeText);
3241
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
2116
3242
  return ` set${service.name}_${nodeCap(member.name)}(value: ${typeText}): void {
2117
- this.setOutputValue("${service.name}", "${member.name}", value);
3243
+ this.setOutputValue("${service.name}", "${member.name}", ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"});
2118
3244
  }`;
2119
3245
  }))
2120
3246
  .join("\n");
@@ -2167,8 +3293,9 @@ function renderNodeExpressWsIndex(spec) {
2167
3293
  .filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
2168
3294
  .map((member) => {
2169
3295
  const typeText = mapTypeTextToTs(member.payloadTypeText);
3296
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
2170
3297
  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 },`;
3298
+ 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
3299
  }
2173
3300
  return ` ${member.name}: {\n set: (value: ${typeText}) => session.set${service.name}_${nodeCap(member.name)}(value)\n },`;
2174
3301
  })
@@ -2180,6 +3307,11 @@ function renderNodeExpressWsIndex(spec) {
2180
3307
  .flatMap((service) => service.members
2181
3308
  .filter((member) => member.kind === "Call" && member.payloadTypeText)
2182
3309
  .map((member) => {
3310
+ const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
3311
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
3312
+ const decodedArgs = member.parameters.length > 0
3313
+ ? member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")
3314
+ : "";
2183
3315
  return ` if (service === "${service.name}" && member === "${member.name}") {
2184
3316
  const handler = implementation.${service.name}.${member.name};
2185
3317
  if (typeof handler !== "function") {
@@ -2202,8 +3334,8 @@ function renderNodeExpressWsIndex(spec) {
2202
3334
  });
2203
3335
  throw err;
2204
3336
  }
2205
- Promise.resolve(handler(buildHandlerBridge(session), ...(args as ${nodeParamTuple(member)})))
2206
- .then((result) => sendJson(session.socket, { type: "callResult", requestId, result }))
3337
+ Promise.resolve(handler(buildHandlerBridge(session)${decodedArgs ? `, ${decodedArgs}` : ""}))
3338
+ .then((result) => sendJson(session.socket, { type: "callResult", requestId, result: ${payloadSite ? `encode${payloadSite.codecId}(result)` : "result"} }))
2207
3339
  .catch((error) => {
2208
3340
  const message = error instanceof Error ? error.message : String(error);
2209
3341
  emitDiagnostic({
@@ -2231,6 +3363,13 @@ function renderNodeExpressWsIndex(spec) {
2231
3363
  .flatMap((service) => service.members
2232
3364
  .filter((member) => member.kind === "Emitter")
2233
3365
  .map((member) => {
3366
+ const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
3367
+ const decodedArgs = member.parameters.length > 0
3368
+ ? member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")
3369
+ : "";
3370
+ const decodedSignalArgs = member.parameters.length > 0
3371
+ ? `[${member.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(args[${index}])` : `args[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ")}]`
3372
+ : "[]";
2234
3373
  return ` if (service === "${service.name}" && member === "${member.name}") {
2235
3374
  const handler = implementation.${service.name}.${member.name};
2236
3375
  if (typeof handler !== "function") {
@@ -2247,7 +3386,8 @@ function renderNodeExpressWsIndex(spec) {
2247
3386
  });
2248
3387
  throw err;
2249
3388
  }
2250
- void Promise.resolve(handler(buildHandlerBridge(session), ...(args as ${nodeParamTuple(member)}))).catch((error) => {
3389
+ session.emitSignal(service, member, ${decodedSignalArgs});
3390
+ void Promise.resolve(handler(buildHandlerBridge(session)${decodedArgs ? `, ${decodedArgs}` : ""})).catch((error) => {
2251
3391
  const message = error instanceof Error ? error.message : String(error);
2252
3392
  emitDiagnostic({
2253
3393
  code: "EmitterHandlerError",
@@ -2268,6 +3408,7 @@ function renderNodeExpressWsIndex(spec) {
2268
3408
  .flatMap((service) => service.members
2269
3409
  .filter((member) => member.kind === "Input" && member.payloadTypeText)
2270
3410
  .map((member) => {
3411
+ const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
2271
3412
  return ` if (service === "${service.name}" && member === "${member.name}") {
2272
3413
  const handler = implementation.${service.name}.${member.name};
2273
3414
  if (typeof handler !== "function") {
@@ -2284,7 +3425,9 @@ function renderNodeExpressWsIndex(spec) {
2284
3425
  });
2285
3426
  throw err;
2286
3427
  }
2287
- void Promise.resolve(handler(buildHandlerBridge(session), value as ${mapTypeTextToTs(member.payloadTypeText)})).catch((error) => {
3428
+ const decodedValue = ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${mapTypeTextToTs(member.payloadTypeText)}`};
3429
+ session.setInputState(service, member, decodedValue);
3430
+ void Promise.resolve(handler(buildHandlerBridge(session), decodedValue)).catch((error) => {
2288
3431
  const message = error instanceof Error ? error.message : String(error);
2289
3432
  emitDiagnostic({
2290
3433
  code: "InputHandlerError",
@@ -2306,6 +3449,9 @@ import type { WebSocket, WebSocketServer } from "ws";
2306
3449
  ${typeImports}
2307
3450
  ${typeDecls}
2308
3451
 
3452
+ // Boundary codec plan helpers
3453
+ ${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
3454
+
2309
3455
  ${handlerInterfaces}
2310
3456
 
2311
3457
  export interface ${spec.widgetName}NodeImplementation {
@@ -2672,7 +3818,6 @@ ${callDispatch}
2672
3818
  const service = String(message.service ?? "");
2673
3819
  const member = String(message.member ?? "");
2674
3820
  const args = Array.isArray(message.args) ? (message.args as unknown[]) : [];
2675
- session.emitSignal(service, member, args);
2676
3821
  ${emitterDispatch}
2677
3822
  const err = new Error(\`No Emitter mapping found for \${service}.\${member}\`);
2678
3823
  emitDiagnostic({
@@ -2691,7 +3836,6 @@ ${emitterDispatch}
2691
3836
  const service = String(message.service ?? "");
2692
3837
  const member = String(message.member ?? "");
2693
3838
  const value = message.value;
2694
- session.setInputState(service, member, value);
2695
3839
  ${inputDispatch}
2696
3840
  const err = new Error(\`No Input mapping found for \${service}.\${member}\`);
2697
3841
  emitDiagnostic({
@@ -2768,7 +3912,23 @@ function renderTypeRootIndexDts(spec) {
2768
3912
  const typeDecls = renderTypeTypesDts(spec).trim();
2769
3913
  const serviceDecls = renderTypeServicesDts(spec).trim();
2770
3914
  const sections = [indexDecls, typeDecls, serviceDecls].filter((s) => s.length > 0);
2771
- return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
3915
+ if (sections.length === 0)
3916
+ return "";
3917
+ const dedupedLines = [];
3918
+ const seenImportLines = new Set();
3919
+ for (const line of sections.join("\n\n").split("\n")) {
3920
+ const trimmed = line.trim();
3921
+ if (!trimmed.startsWith("import type ")) {
3922
+ dedupedLines.push(line);
3923
+ continue;
3924
+ }
3925
+ if (seenImportLines.has(trimmed))
3926
+ continue;
3927
+ seenImportLines.add(trimmed);
3928
+ dedupedLines.push(line);
3929
+ }
3930
+ const deduped = dedupedLines.join("\n").replace(/\n{3,}/g, "\n\n");
3931
+ return `${deduped}\n`;
2772
3932
  }
2773
3933
  function generatedCppLibraryDirName(widgetName) {
2774
3934
  return (0, layout_1.generatedQtWidgetDirName)(widgetName);
@@ -2781,10 +3941,11 @@ function generateOutputs(spec, options = { emitQWidget: true, emitAngularService
2781
3941
  const cppDir = `backend/cpp/qt/${generatedCppLibraryDirName(spec.widgetName)}`;
2782
3942
  const nodeDir = `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}`;
2783
3943
  const outputs = {};
3944
+ const codecCatalog = (0, boundary_codecs_1.buildBoundaryCodecCatalog)(spec);
2784
3945
  if (options.emitAngularService) {
2785
3946
  outputs[`${frontendDir}/package.json`] = renderNpmPackage(spec);
2786
3947
  outputs[`${frontendDir}/index.ts`] = renderTsIndex();
2787
- outputs[`${frontendDir}/services.ts`] = renderTsServices(spec);
3948
+ outputs[`${frontendDir}/services.ts`] = renderTsServices(spec, codecCatalog);
2788
3949
  outputs[`${frontendDir}/types.ts`] = renderTsTypes(spec);
2789
3950
  outputs[`${frontendDir}/index.js`] = renderJsIndex();
2790
3951
  outputs[`${frontendDir}/services.js`] = renderJsServices();
@@ -2798,13 +3959,13 @@ function generateOutputs(spec, options = { emitQWidget: true, emitAngularService
2798
3959
  outputs[`${cppDir}/CMakeLists.txt`] = renderCMake(spec);
2799
3960
  outputs[`${cppDir}/${spec.widgetName}.qrc`] = renderEmbeddedQrc(spec.widgetName, []);
2800
3961
  outputs[`${cppDir}/include/${spec.widgetName}.h`] = renderWidgetUmbrellaHeader(spec);
2801
- outputs[`${cppDir}/include/${spec.widgetName}Widget.h`] = renderWidgetHeader(spec, cppTypes);
3962
+ outputs[`${cppDir}/include/${spec.widgetName}Widget.h`] = renderWidgetHeader(spec, cppTypes, codecCatalog);
2802
3963
  outputs[`${cppDir}/include/${spec.widgetName}Types.h`] = renderTypesHeader(spec, cppTypes);
2803
- outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes);
3964
+ outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes, codecCatalog);
2804
3965
  }
2805
3966
  if (options.emitNodeExpressWs) {
2806
3967
  outputs[`${nodeDir}/package.json`] = renderNodeExpressWsPackage(spec);
2807
- outputs[`${nodeDir}/index.ts`] = renderNodeExpressWsIndex(spec);
3968
+ outputs[`${nodeDir}/index.ts`] = renderNodeExpressWsIndex(spec, codecCatalog);
2808
3969
  outputs[`${nodeDir}/types/index.d.ts`] = renderNodeExpressWsTypes(spec);
2809
3970
  }
2810
3971
  return outputs;
@@ -2985,13 +4146,15 @@ function renderQtIntegrationCMake(widgetName) {
2985
4146
  const generatedRootVar = "ANQST_GENERATED_WIDGET_DIR";
2986
4147
  const generatedIncludeVar = "ANQST_GENERATED_INCLUDE_DIR";
2987
4148
  const projectRootVar = "ANQST_PROJECT_ROOT";
4149
+ const requiredFilesVar = "ANQST_REQUIRED_GENERATED_FILES";
4150
+ const widgetBinaryDirVar = "ANQST_GENERATED_WIDGET_BINARY_DIR";
2988
4151
  const widgetTarget = `${widgetName}Widget`;
2989
- const autogenTarget = `${widgetTarget}_anqst_codegen`;
2990
4152
  return `cmake_minimum_required(VERSION 3.21)
2991
4153
 
2992
4154
  set(${projectRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../../../../..")
2993
4155
  set(${generatedRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../qt/${generatedCppLibraryDirName(widgetName)}")
2994
4156
  set(${generatedIncludeVar} "\${${generatedRootVar}}/include")
4157
+ set(${widgetBinaryDirVar} "\${CMAKE_CURRENT_BINARY_DIR}/${generatedCppLibraryDirName(widgetName)}")
2995
4158
 
2996
4159
  if(TARGET ${widgetTarget})
2997
4160
  return()
@@ -3001,66 +4164,27 @@ if(NOT TARGET anqstwebhostbase)
3001
4164
  message(FATAL_ERROR "Target 'anqstwebhostbase' must exist before including generated AnQst CMake for ${widgetName}.")
3002
4165
  endif()
3003
4166
 
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(
4167
+ set(${requiredFilesVar}
4168
+ "\${${generatedRootVar}}/CMakeLists.txt"
3040
4169
  "\${${generatedRootVar}}/${widgetName}.qrc"
3041
4170
  "\${${generatedRootVar}}/${widgetName}.cpp"
3042
4171
  "\${${generatedIncludeVar}}/${widgetName}.h"
3043
4172
  "\${${generatedIncludeVar}}/${widgetName}Widget.h"
3044
4173
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
3045
- PROPERTIES GENERATED TRUE
4174
+ "\${${generatedRootVar}}/webapp/index.html"
3046
4175
  )
3047
4176
 
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
- )
4177
+ foreach(required_file IN LISTS ${requiredFilesVar})
4178
+ if(NOT EXISTS "\${required_file}")
4179
+ message(FATAL_ERROR
4180
+ "Generated AnQst widget tree is incomplete for ${widgetName}. "
4181
+ "Missing file: \${required_file}. "
4182
+ "Run 'npx anqst build' in '\${${projectRootVar}}' first."
4183
+ )
4184
+ endif()
4185
+ endforeach()
4186
+
4187
+ add_subdirectory("\${${generatedRootVar}}" "\${${widgetBinaryDirVar}}")
3064
4188
  `;
3065
4189
  }
3066
4190
  function installQtIntegrationCMake(cwd, widgetName) {