@dusted/anqst 1.5.0 → 1.6.0

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