@dusted/anqst 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/emit.js CHANGED
@@ -12,6 +12,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
12
12
  const node_path_1 = __importDefault(require("node:path"));
13
13
  const typescript_1 = __importDefault(require("typescript"));
14
14
  const pngjs_1 = require("pngjs");
15
+ const build_stamp_1 = require("./build-stamp");
15
16
  const layout_1 = require("./layout");
16
17
  function stripAnQstType(typeText) {
17
18
  return typeText
@@ -652,13 +653,24 @@ ${decls}
652
653
  ${metatypes}
653
654
  `;
654
655
  }
656
+ function renderWidgetUmbrellaHeader(spec) {
657
+ return `#pragma once
658
+ // Built by <AnQst_version>
659
+ #include "${spec.widgetName}Widget.h"
660
+ #include "${spec.widgetName}Types.h"
661
+ `;
662
+ }
655
663
  function renderWidgetHeader(spec, cppTypes) {
664
+ const widgetClassName = `${spec.widgetName}Widget`;
656
665
  const callbackAliases = [];
657
666
  const publicMethods = [];
667
+ const slotMethods = [];
668
+ const handleMethods = [];
669
+ const callSetterMethods = [];
658
670
  const signals = [];
659
671
  const properties = [];
660
672
  const fields = [];
661
- const outputSetters = [];
673
+ const publicSlots = [];
662
674
  const bindings = [];
663
675
  for (const service of spec.services) {
664
676
  for (const member of service.members) {
@@ -666,21 +678,20 @@ function renderWidgetHeader(spec, cppTypes) {
666
678
  const memberPascal = pascalCase(member.name);
667
679
  if (member.kind === "Call" && member.payloadTypeText) {
668
680
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
669
- const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
681
+ const args = member.parameters.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
670
682
  callbackAliases.push(`using ${memberPascal}Handler = std::function<${cppType}(${args})>;`);
671
- publicMethods.push(`void set${memberPascal}Handler(const ${memberPascal}Handler& handler);`);
683
+ handleMethods.push(` void ${member.name}(const ${memberPascal}Handler& handler) const;`);
684
+ callSetterMethods.push(`void set${memberPascal}CallHandler(const ${memberPascal}Handler& handler);`);
672
685
  fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
673
686
  }
674
687
  else if (member.kind === "Emitter") {
675
- const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
676
- callbackAliases.push(`using ${memberPascal}Handler = std::function<void(${args})>;`);
677
- publicMethods.push(`void set${memberPascal}Handler(const ${memberPascal}Handler& handler);`);
678
- fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
688
+ const args = member.parameters.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
689
+ signals.push(`void ${member.name}(${args});`);
679
690
  }
680
691
  else if (member.kind === "Slot") {
681
692
  const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
682
693
  const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
683
- publicMethods.push(`${ret} ${member.name}(${args}${args ? ", " : ""}bool* ok = nullptr, QString* error = nullptr);`);
694
+ slotMethods.push(`${ret} slot_${member.name}(${args});`);
684
695
  }
685
696
  else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
686
697
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
@@ -696,13 +707,16 @@ function renderWidgetHeader(spec, cppTypes) {
696
707
  fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
697
708
  }
698
709
  else {
699
- outputSetters.push(`void publish${memberPascal}(const ${cppType}& value);`);
710
+ publicSlots.push(`void ${member.name}Slot(const ${cppType}& value);`);
700
711
  }
701
712
  }
702
713
  }
703
714
  }
704
715
  return `#pragma once
716
+ #include <QDateTime>
705
717
  #include <QHash>
718
+ #include <QMetaMethod>
719
+ #include <QQueue>
706
720
  #include <QVariant>
707
721
  #include <QVariantList>
708
722
  #include <functional>
@@ -710,28 +724,54 @@ function renderWidgetHeader(spec, cppTypes) {
710
724
  #include "${spec.widgetName}Types.h"
711
725
 
712
726
  namespace ${spec.widgetName} {
727
+ } // namespace ${spec.widgetName}
728
+
729
+ using namespace ${spec.widgetName};
713
730
 
714
- class ${spec.widgetName} : public AnQstWebHostBase {
731
+ class ${widgetClassName} : public AnQstWebHostBase {
715
732
  Q_OBJECT
716
733
  ${properties.map((p) => ` ${p}`).join("\n")}
717
734
 
718
735
  public:
719
- explicit ${spec.widgetName}(QWidget* parent = nullptr);
720
- ~${spec.widgetName}() override;
736
+ ${callbackAliases.map((s) => ` ${s}`).join("\n")}
737
+
738
+ class handle {
739
+ public:
740
+ explicit handle(${widgetClassName}* owner) : m_owner(owner) {}
741
+ ${handleMethods.join("\n")}
742
+ private:
743
+ ${widgetClassName}* m_owner;
744
+ };
745
+
746
+ explicit ${widgetClassName}(QWidget* parent = nullptr);
747
+ ~${widgetClassName}() override;
721
748
  bool enableDebug();
722
749
  static constexpr const char* kBootstrapEntryPoint = "index.html";
723
750
  static constexpr const char* kBootstrapContentRoot = "qrc:/${spec.widgetName.toLowerCase()}";
724
751
  static constexpr const char* kBootstrapBridgeObject = "${spec.widgetName}Bridge";
752
+ static constexpr int kMaxQueuedCallsPerEndpoint = 1024;
725
753
 
726
- ${callbackAliases.map((s) => ` ${s}`).join("\n")}
754
+ handle handle;
727
755
  ${publicMethods.map((s) => ` ${s}`).join("\n")}
728
- ${outputSetters.map((s) => ` ${s}`).join("\n")}
756
+
757
+ public slots:
758
+ ${slotMethods.map((s) => ` ${s}`).join("\n")}
759
+ ${publicSlots.map((s) => ` ${s}`).join("\n")}
729
760
 
730
761
  signals:
731
762
  ${signals.map((s) => ` ${s}`).join("\n")}
732
763
  void diagnosticsForwarded(const QVariantMap& payload);
733
764
 
765
+ protected:
766
+ void connectNotify(const QMetaMethod& signal) override;
767
+ void disconnectNotify(const QMetaMethod& signal) override;
768
+
734
769
  private:
770
+ struct PendingCallInvocation {
771
+ QString requestId;
772
+ QVariantList args;
773
+ QDateTime enqueuedAt;
774
+ };
735
775
  struct BridgeBindingRow {
736
776
  const char* service;
737
777
  const char* member;
@@ -741,21 +781,37 @@ private:
741
781
  static constexpr int kBridgeBindingsCount = ${bindings.length};
742
782
  static QString makeBindingKey(const QString& service, const QString& member);
743
783
  void installBridgeBindings();
784
+ bool hasEmitterListeners(const QString& service, const QString& member) const;
744
785
  QVariant handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args);
745
786
  void handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args);
746
787
  void handleGeneratedInput(const QString& service, const QString& member, const QVariant& value);
747
-
788
+ QVariant waitForCallHandlerAndInvoke(
789
+ const QString& service,
790
+ const QString& member,
791
+ const QString& requestId,
792
+ int timeoutMs,
793
+ const std::function<QVariant()>& invokeNow);
794
+ void removeQueuedCallById(const QString& queueKey, const QString& requestId);
795
+ ${callSetterMethods.map((s) => ` ${s}`).join("\n")}
796
+
797
+ qulonglong m_callRequestCounter{0};
798
+ QHash<QString, QQueue<PendingCallInvocation>> m_queuedCalls;
748
799
  ${fields.map((f) => ` ${f}`).join("\n")}
749
800
  };
750
-
751
- } // namespace ${spec.widgetName}
752
801
  `;
753
802
  }
754
803
  function renderCppStub(spec, cppTypes) {
804
+ const widgetClassName = `${spec.widgetName}Widget`;
755
805
  const lines = [];
756
- lines.push(`#include "include/${spec.widgetName}.h"`);
806
+ lines.push(`#include "include/${spec.widgetName}Widget.h"`);
757
807
  lines.push(`#include <QDebug>`);
808
+ lines.push(`#include <QElapsedTimer>`);
809
+ lines.push(`#include <QEventLoop>`);
758
810
  lines.push(`#include <QMetaType>`);
811
+ lines.push(`#include <QTimer>`);
812
+ lines.push(`#include <stdexcept>`);
813
+ lines.push("");
814
+ lines.push(`using namespace ${spec.widgetName};`);
759
815
  lines.push("");
760
816
  lines.push(`extern int qInitResources_${spec.widgetName}();`);
761
817
  lines.push("");
@@ -772,9 +828,23 @@ function renderCppStub(spec, cppTypes) {
772
828
  lines.push("}");
773
829
  lines.push("}");
774
830
  lines.push("");
775
- lines.push(`namespace ${spec.widgetName} {`);
776
- lines.push("");
777
- lines.push(`const ${spec.widgetName}::BridgeBindingRow ${spec.widgetName}::kBridgeBindings[] = {`);
831
+ for (const service of spec.services) {
832
+ for (const member of service.members) {
833
+ if (member.kind !== "Call" || !member.payloadTypeText)
834
+ continue;
835
+ const pascal = pascalCase(member.name);
836
+ lines.push(`void ${widgetClassName}::handle::${member.name}(const ${pascal}Handler& handler) const {`);
837
+ lines.push(` if (m_owner == nullptr) return;`);
838
+ lines.push(` m_owner->set${pascal}CallHandler(handler);`);
839
+ lines.push(`}`);
840
+ lines.push("");
841
+ lines.push(`void ${widgetClassName}::set${pascal}CallHandler(const ${pascal}Handler& handler) {`);
842
+ lines.push(` m_${member.name}Handler = handler;`);
843
+ lines.push(`}`);
844
+ lines.push("");
845
+ }
846
+ }
847
+ lines.push(`const ${widgetClassName}::BridgeBindingRow ${widgetClassName}::kBridgeBindings[] = {`);
778
848
  for (const service of spec.services) {
779
849
  for (const member of service.members) {
780
850
  lines.push(` {"${service.name}", "${member.name}", "${member.kind}"},`);
@@ -782,7 +852,7 @@ function renderCppStub(spec, cppTypes) {
782
852
  }
783
853
  lines.push(`};`);
784
854
  lines.push("");
785
- lines.push(`${spec.widgetName}::${spec.widgetName}(QWidget* parent) : AnQstWebHostBase(parent) {`);
855
+ lines.push(`${widgetClassName}::${widgetClassName}(QWidget* parent) : AnQstWebHostBase(parent), handle(this) {`);
786
856
  lines.push(` static const bool kResourcesInitialized = []() {`);
787
857
  lines.push(` ::qInitResources_${spec.widgetName}();`);
788
858
  lines.push(` return true;`);
@@ -790,7 +860,7 @@ function renderCppStub(spec, cppTypes) {
790
860
  lines.push(` Q_UNUSED(kResourcesInitialized);`);
791
861
  lines.push(` registerGeneratedMetaTypes();`);
792
862
  lines.push(` installBridgeBindings();`);
793
- lines.push(` QObject::connect(this, &AnQstWebHostBase::onHostError, this, &${spec.widgetName}::diagnosticsForwarded);`);
863
+ lines.push(` QObject::connect(this, &AnQstWebHostBase::onHostError, this, &${widgetClassName}::diagnosticsForwarded);`);
794
864
  lines.push(` const bool rootOk = setContentRoot(QString::fromUtf8(kBootstrapContentRoot));`);
795
865
  lines.push(` const bool bridgeOk = setBridgeObject(this, QString::fromUtf8(kBootstrapBridgeObject));`);
796
866
  lines.push(` const bool loadOk = rootOk && bridgeOk && loadEntryPoint(QString::fromUtf8(kBootstrapEntryPoint));`);
@@ -799,17 +869,74 @@ function renderCppStub(spec, cppTypes) {
799
869
  lines.push(` }`);
800
870
  lines.push("}");
801
871
  lines.push("");
802
- lines.push(`${spec.widgetName}::~${spec.widgetName}() = default;`);
872
+ lines.push(`${widgetClassName}::~${widgetClassName}() = default;`);
803
873
  lines.push("");
804
- lines.push(`bool ${spec.widgetName}::enableDebug() {`);
874
+ lines.push(`bool ${widgetClassName}::enableDebug() {`);
805
875
  lines.push(` return AnQstWebHostBase::enableDebug();`);
806
876
  lines.push("}");
807
877
  lines.push("");
808
- lines.push(`QString ${spec.widgetName}::makeBindingKey(const QString& service, const QString& member) {`);
878
+ lines.push(`QString ${widgetClassName}::makeBindingKey(const QString& service, const QString& member) {`);
809
879
  lines.push(` return service + QStringLiteral("::") + member;`);
810
880
  lines.push(`}`);
811
881
  lines.push("");
812
- lines.push(`void ${spec.widgetName}::installBridgeBindings() {`);
882
+ lines.push(`void ${widgetClassName}::removeQueuedCallById(const QString& queueKey, const QString& requestId) {`);
883
+ lines.push(` if (!m_queuedCalls.contains(queueKey)) return;`);
884
+ lines.push(` auto& queue = m_queuedCalls[queueKey];`);
885
+ lines.push(` for (int i = 0; i < queue.size(); ++i) {`);
886
+ lines.push(` if (queue[i].requestId == requestId) {`);
887
+ lines.push(` queue.removeAt(i);`);
888
+ lines.push(` break;`);
889
+ lines.push(` }`);
890
+ lines.push(` }`);
891
+ lines.push(`}`);
892
+ lines.push("");
893
+ lines.push(`QVariant ${widgetClassName}::waitForCallHandlerAndInvoke(`);
894
+ lines.push(` const QString& service,`);
895
+ lines.push(` const QString& member,`);
896
+ lines.push(` const QString& requestId,`);
897
+ lines.push(` int timeoutMs,`);
898
+ lines.push(` const std::function<QVariant()>& invokeNow) {`);
899
+ lines.push(` const QString queueKey = makeBindingKey(service, member);`);
900
+ lines.push(` QElapsedTimer timer;`);
901
+ lines.push(` timer.start();`);
902
+ lines.push(` QEventLoop loop;`);
903
+ lines.push(` QTimer tick;`);
904
+ lines.push(` tick.setSingleShot(true);`);
905
+ lines.push(` QObject::connect(&tick, &QTimer::timeout, &loop, &QEventLoop::quit);`);
906
+ lines.push(` while (true) {`);
907
+ lines.push(` if (m_queuedCalls.contains(queueKey) && !m_queuedCalls[queueKey].isEmpty() && m_queuedCalls[queueKey].head().requestId == requestId) {`);
908
+ lines.push(` m_queuedCalls[queueKey].dequeue();`);
909
+ lines.push(` return invokeNow();`);
910
+ lines.push(` }`);
911
+ lines.push(` if (timeoutMs > 0 && timer.elapsed() >= timeoutMs) {`);
912
+ lines.push(` removeQueuedCallById(queueKey, requestId);`);
913
+ lines.push(` return QVariantMap{`);
914
+ lines.push(` {QStringLiteral("code"), QStringLiteral("BridgeTimeoutError")},`);
915
+ lines.push(` {QStringLiteral("message"), QStringLiteral("Call timed out while waiting for callback registration.")},`);
916
+ lines.push(` {QStringLiteral("service"), service},`);
917
+ lines.push(` {QStringLiteral("member"), member},`);
918
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
919
+ lines.push(` };`);
920
+ lines.push(` }`);
921
+ lines.push(` tick.start(10);`);
922
+ lines.push(` loop.exec();`);
923
+ lines.push(` }`);
924
+ lines.push(`}`);
925
+ lines.push("");
926
+ lines.push(`bool ${widgetClassName}::hasEmitterListeners(const QString& service, const QString& member) const {`);
927
+ for (const service of spec.services) {
928
+ for (const member of service.members) {
929
+ if (member.kind !== "Emitter")
930
+ continue;
931
+ lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
932
+ lines.push(` return isSignalConnected(QMetaMethod::fromSignal(&${widgetClassName}::${member.name}));`);
933
+ lines.push(` }`);
934
+ }
935
+ }
936
+ lines.push(` return false;`);
937
+ lines.push(`}`);
938
+ lines.push("");
939
+ lines.push(`void ${widgetClassName}::installBridgeBindings() {`);
813
940
  lines.push(` setCallHandler([this](const QString& service, const QString& member, const QVariantList& args) -> QVariant {`);
814
941
  lines.push(` return handleGeneratedCall(service, member, args);`);
815
942
  lines.push(` });`);
@@ -821,55 +948,97 @@ function renderCppStub(spec, cppTypes) {
821
948
  lines.push(` });`);
822
949
  lines.push(`}`);
823
950
  lines.push("");
824
- lines.push(`QVariant ${spec.widgetName}::handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args) {`);
951
+ lines.push(`QVariant ${widgetClassName}::handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args) {`);
825
952
  for (const service of spec.services) {
826
953
  for (const member of service.members) {
827
954
  if (member.kind !== "Call" || !member.payloadTypeText)
828
955
  continue;
956
+ const timeoutMs = member.timeoutMs;
829
957
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
830
- const pascal = pascalCase(member.name);
831
958
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
832
- lines.push(` if (!m_${member.name}Handler) {`);
833
- lines.push(` if (property("anqstDesignerContext").toBool()) {`);
834
- lines.push(` return ${cppToVariantExpression(cppType, designerPlaceholderCppExpression(cppType, member.name))};`);
835
- lines.push(` }`);
836
- lines.push(` return QVariant();`);
837
- lines.push(` }`);
838
959
  for (let i = 0; i < member.parameters.length; i++) {
839
960
  const p = member.parameters[i];
840
961
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
841
962
  lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
842
963
  }
843
- const argNames = member.parameters.map((p) => p.name).join(", ");
844
- lines.push(` const ${cppType} result = m_${member.name}Handler(${argNames});`);
845
- lines.push(` return ${cppToVariantExpression(cppType, "result")};`);
964
+ lines.push(` const QString requestId = QStringLiteral("call-%1").arg(++m_callRequestCounter);`);
965
+ lines.push(` const QString queueKey = makeBindingKey(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"));`);
966
+ lines.push(` auto invokeNow = [this, requestId${member.parameters.length > 0 ? `, ${member.parameters.map((p) => p.name).join(", ")}` : ""}]() -> QVariant {`);
967
+ lines.push(` if (!m_${member.name}Handler) {`);
968
+ lines.push(` return QVariantMap{`);
969
+ lines.push(` {QStringLiteral("code"), QStringLiteral("HandlerNotRegisteredError")},`);
970
+ lines.push(` {QStringLiteral("message"), QStringLiteral("No callback registered for Call endpoint.")},`);
971
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
972
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
973
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
974
+ lines.push(` };`);
975
+ lines.push(` }`);
976
+ lines.push(` try {`);
977
+ const callArgs = member.parameters.map((p) => p.name).join(", ");
978
+ lines.push(` const ${cppType} result = m_${member.name}Handler(${callArgs});`);
979
+ lines.push(` return ${cppToVariantExpression(cppType, "result")};`);
980
+ lines.push(` } catch (const std::exception& ex) {`);
981
+ lines.push(` return QVariantMap{`);
982
+ lines.push(` {QStringLiteral("code"), QStringLiteral("CallHandlerError")},`);
983
+ lines.push(` {QStringLiteral("message"), QString::fromUtf8(ex.what())},`);
984
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
985
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
986
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
987
+ lines.push(` };`);
988
+ lines.push(` } catch (...) {`);
989
+ lines.push(` return QVariantMap{`);
990
+ lines.push(` {QStringLiteral("code"), QStringLiteral("CallHandlerError")},`);
991
+ lines.push(` {QStringLiteral("message"), QStringLiteral("Call handler threw unknown exception.")},`);
992
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
993
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
994
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
995
+ lines.push(` };`);
996
+ lines.push(` }`);
997
+ lines.push(` };`);
998
+ lines.push(` if (m_${member.name}Handler) {`);
999
+ lines.push(` return invokeNow();`);
1000
+ lines.push(` }`);
1001
+ lines.push(` auto& queue = m_queuedCalls[queueKey];`);
1002
+ lines.push(` if (queue.size() >= kMaxQueuedCallsPerEndpoint) {`);
1003
+ lines.push(` queue.dequeue();`);
1004
+ lines.push(` }`);
1005
+ lines.push(` queue.enqueue(PendingCallInvocation{requestId, args, QDateTime::currentDateTimeUtc()});`);
1006
+ lines.push(` return waitForCallHandlerAndInvoke(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), requestId, ${timeoutMs}, invokeNow);`);
846
1007
  lines.push(` }`);
847
1008
  }
848
1009
  }
849
- lines.push(` return QVariant();`);
1010
+ lines.push(` return QVariantMap{`);
1011
+ lines.push(` {QStringLiteral("code"), QStringLiteral("HandlerNotRegisteredError")},`);
1012
+ lines.push(` {QStringLiteral("message"), QStringLiteral("No Call mapping found.")},`);
1013
+ lines.push(` {QStringLiteral("service"), service},`);
1014
+ lines.push(` {QStringLiteral("member"), member},`);
1015
+ lines.push(` {QStringLiteral("requestId"), QString()}`);
1016
+ lines.push(` };`);
850
1017
  lines.push(`}`);
851
1018
  lines.push("");
852
- lines.push(`void ${spec.widgetName}::handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args) {`);
1019
+ lines.push(`void ${widgetClassName}::handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args) {`);
1020
+ lines.push(` if (!hasEmitterListeners(service, member)) {`);
1021
+ lines.push(` return;`);
1022
+ lines.push(` }`);
853
1023
  for (const service of spec.services) {
854
1024
  for (const member of service.members) {
855
1025
  if (member.kind !== "Emitter")
856
1026
  continue;
857
1027
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
858
- lines.push(` if (!m_${member.name}Handler) return;`);
859
1028
  for (let i = 0; i < member.parameters.length; i++) {
860
1029
  const p = member.parameters[i];
861
1030
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
862
1031
  lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
863
1032
  }
864
1033
  const argNames = member.parameters.map((p) => p.name).join(", ");
865
- lines.push(` m_${member.name}Handler(${argNames});`);
1034
+ lines.push(` emit ${member.name}(${argNames});`);
866
1035
  lines.push(` return;`);
867
1036
  lines.push(` }`);
868
1037
  }
869
1038
  }
870
1039
  lines.push(`}`);
871
1040
  lines.push("");
872
- lines.push(`void ${spec.widgetName}::handleGeneratedInput(const QString& service, const QString& member, const QVariant& value) {`);
1041
+ lines.push(`void ${widgetClassName}::handleGeneratedInput(const QString& service, const QString& member, const QVariant& value) {`);
873
1042
  for (const service of spec.services) {
874
1043
  for (const member of service.members) {
875
1044
  if (member.kind !== "Input" || !member.payloadTypeText)
@@ -888,20 +1057,8 @@ function renderCppStub(spec, cppTypes) {
888
1057
  for (const service of spec.services) {
889
1058
  for (const member of service.members) {
890
1059
  const memberPascal = pascalCase(member.name);
891
- if ((member.kind === "Call" || member.kind === "Input") && member.payloadTypeText) {
892
- lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
893
- lines.push(` m_${member.name}Handler = handler;`);
894
- lines.push("}");
895
- lines.push("");
896
- }
897
- else if (member.kind === "Emitter") {
898
- lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
899
- lines.push(` m_${member.name}Handler = handler;`);
900
- lines.push("}");
901
- lines.push("");
902
- }
903
- else if (member.kind === "Input" && member.payloadTypeText) {
904
- lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
1060
+ if (member.kind === "Input" && member.payloadTypeText) {
1061
+ lines.push(`void ${widgetClassName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
905
1062
  lines.push(` m_${member.name}Handler = handler;`);
906
1063
  lines.push("}");
907
1064
  lines.push("");
@@ -909,8 +1066,7 @@ function renderCppStub(spec, cppTypes) {
909
1066
  if (member.kind === "Slot") {
910
1067
  const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
911
1068
  const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
912
- const argsWithMeta = `${args}${args ? ", " : ""}bool* ok, QString* error`;
913
- lines.push(`${ret} ${spec.widgetName}::${member.name}(${argsWithMeta}) {`);
1069
+ lines.push(`${ret} ${widgetClassName}::slot_${member.name}(${args}) {`);
914
1070
  lines.push(` QVariantList invokeArgs;`);
915
1071
  for (const p of member.parameters) {
916
1072
  const pType = mapTsTypeToCpp(p.typeText);
@@ -919,14 +1075,18 @@ function renderCppStub(spec, cppTypes) {
919
1075
  lines.push(` QVariant result;`);
920
1076
  lines.push(` QString invokeError;`);
921
1077
  lines.push(` const bool success = invokeSlot(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), invokeArgs, &result, &invokeError);`);
922
- lines.push(` if (ok != nullptr) *ok = success;`);
923
- lines.push(` if (error != nullptr) *error = invokeError;`);
1078
+ lines.push(` if (!success) {`);
1079
+ lines.push(` if (invokeError == QStringLiteral("slot invocation timeout")) {`);
1080
+ lines.push(` const QString timeoutMsg = QStringLiteral("[Timeout] ${service.name}.${member.name}: The webapp inside the widget did not anwser within %1 ms.").arg(slotInvocationTimeoutMs());`);
1081
+ lines.push(` throw std::runtime_error(timeoutMsg.toStdString());`);
1082
+ lines.push(` }`);
1083
+ lines.push(` const QString requestFailed = QStringLiteral("[RequestFailed]: %1").arg(invokeError);`);
1084
+ lines.push(` throw std::runtime_error(requestFailed.toStdString());`);
1085
+ lines.push(` }`);
924
1086
  if (ret === "void") {
925
- lines.push(` if (!success) return;`);
926
1087
  lines.push(` return;`);
927
1088
  }
928
1089
  else {
929
- lines.push(` if (!success) return ${ret}{};`);
930
1090
  lines.push(` return ${variantToCppExpression(ret, "result")};`);
931
1091
  }
932
1092
  lines.push("}");
@@ -935,11 +1095,11 @@ function renderCppStub(spec, cppTypes) {
935
1095
  else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
936
1096
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
937
1097
  const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
938
- lines.push(`${cppType} ${spec.widgetName}::${member.name}() const {`);
1098
+ lines.push(`${cppType} ${widgetClassName}::${member.name}() const {`);
939
1099
  lines.push(` return m_${member.name};`);
940
1100
  lines.push("}");
941
1101
  lines.push("");
942
- lines.push(`void ${spec.widgetName}::set${cap}(const ${cppType}& value) {`);
1102
+ lines.push(`void ${widgetClassName}::set${cap}(const ${cppType}& value) {`);
943
1103
  lines.push(` if (m_${member.name} == value) return;`);
944
1104
  lines.push(` m_${member.name} = value;`);
945
1105
  if (member.kind === "Output") {
@@ -949,7 +1109,7 @@ function renderCppStub(spec, cppTypes) {
949
1109
  lines.push("}");
950
1110
  lines.push("");
951
1111
  if (member.kind === "Output") {
952
- lines.push(`void ${spec.widgetName}::publish${pascalCase(member.name)}(const ${cppType}& value) {`);
1112
+ lines.push(`void ${widgetClassName}::${member.name}Slot(const ${cppType}& value) {`);
953
1113
  lines.push(` set${cap}(value);`);
954
1114
  lines.push(`}`);
955
1115
  lines.push("");
@@ -957,7 +1117,16 @@ function renderCppStub(spec, cppTypes) {
957
1117
  }
958
1118
  }
959
1119
  }
960
- lines.push(`} // namespace ${spec.widgetName}`);
1120
+ lines.push(`void ${widgetClassName}::connectNotify(const QMetaMethod& signal) {`);
1121
+ lines.push(` AnQstWebHostBase::connectNotify(signal);`);
1122
+ lines.push(` Q_UNUSED(signal);`);
1123
+ lines.push(`}`);
1124
+ lines.push("");
1125
+ lines.push(`void ${widgetClassName}::disconnectNotify(const QMetaMethod& signal) {`);
1126
+ lines.push(` AnQstWebHostBase::disconnectNotify(signal);`);
1127
+ lines.push(` Q_UNUSED(signal);`);
1128
+ lines.push(`}`);
1129
+ lines.push("");
961
1130
  return lines.join("\n");
962
1131
  }
963
1132
  function renderCMake(spec) {
@@ -977,6 +1146,7 @@ add_library(${spec.widgetName}Widget
977
1146
  ${spec.widgetName}.cpp
978
1147
  ${spec.widgetName}.qrc
979
1148
  include/${spec.widgetName}.h
1149
+ include/${spec.widgetName}Widget.h
980
1150
  include/${spec.widgetName}Types.h
981
1151
  )
982
1152
  target_include_directories(${spec.widgetName}Widget
@@ -1002,24 +1172,7 @@ function normalizeSlashes(value) {
1002
1172
  return value.split(node_path_1.default.sep).join("/");
1003
1173
  }
1004
1174
  function resolveActiveBuildStamp() {
1005
- const fromEnv = process.env.ANQST_BUILD_STAMP?.trim();
1006
- if (fromEnv && fromEnv.length > 0) {
1007
- return fromEnv;
1008
- }
1009
- const activePath = node_path_1.default.resolve(__dirname, "..", "..", ".anqstgen-version-active.json");
1010
- if (!node_fs_1.default.existsSync(activePath)) {
1011
- return "";
1012
- }
1013
- try {
1014
- const parsed = JSON.parse(node_fs_1.default.readFileSync(activePath, "utf8"));
1015
- if (typeof parsed.active === "string" && parsed.active.trim().length > 0) {
1016
- return parsed.active.trim();
1017
- }
1018
- }
1019
- catch {
1020
- return "";
1021
- }
1022
- return "";
1175
+ return build_stamp_1.ANQST_BUILD_STAMP.trim();
1023
1176
  }
1024
1177
  function withBuildStamp(relativePath, content) {
1025
1178
  const stamp = resolveActiveBuildStamp();
@@ -1114,6 +1267,12 @@ function renderLocalTypeImports(spec) {
1114
1267
  return "";
1115
1268
  return `import type { ${localTypeNames.join(", ")} } from "./types";`;
1116
1269
  }
1270
+ function slotHandlerReturnType(tsRet) {
1271
+ if (tsRet === "void") {
1272
+ return "void | Promise<void> | Error";
1273
+ }
1274
+ return `${tsRet} | Promise<${tsRet}> | Error`;
1275
+ }
1117
1276
  function renderTsService(spec, serviceName) {
1118
1277
  const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
1119
1278
  const fieldLines = [];
@@ -1137,7 +1296,7 @@ function renderTsService(spec, serviceName) {
1137
1296
  }
1138
1297
  if (m.kind === "Slot") {
1139
1298
  const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
1140
- onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${ret}): void => {`);
1299
+ onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
1141
1300
  onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", handler as (...args: unknown[]) => unknown);`);
1142
1301
  onSlotMembers.push(" },");
1143
1302
  continue;
@@ -1197,7 +1356,7 @@ function renderTsServiceDts(spec, serviceName) {
1197
1356
  }
1198
1357
  if (m.kind === "Slot") {
1199
1358
  const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
1200
- onSlotMembers.push(` ${m.name}(handler: (${args}) => ${ret}): void;`);
1359
+ onSlotMembers.push(` ${m.name}(handler: (${args}) => ${slotHandlerReturnType(ret)}): void;`);
1201
1360
  continue;
1202
1361
  }
1203
1362
  if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
@@ -1267,6 +1426,24 @@ interface BridgeAdapter {
1267
1426
  onSlotInvocation(handler: SlotInvocationListener): void;
1268
1427
  }
1269
1428
 
1429
+ function isBridgeCallError(value: unknown): value is {
1430
+ code: unknown;
1431
+ message: unknown;
1432
+ service: unknown;
1433
+ member: unknown;
1434
+ requestId: unknown;
1435
+ } {
1436
+ if (value === null || typeof value !== "object") return false;
1437
+ const row = value as Record<string, unknown>;
1438
+ return (
1439
+ Object.prototype.hasOwnProperty.call(row, "code")
1440
+ && Object.prototype.hasOwnProperty.call(row, "message")
1441
+ && Object.prototype.hasOwnProperty.call(row, "service")
1442
+ && Object.prototype.hasOwnProperty.call(row, "member")
1443
+ && Object.prototype.hasOwnProperty.call(row, "requestId")
1444
+ );
1445
+ }
1446
+
1270
1447
  class QtWebChannelAdapter implements BridgeAdapter {
1271
1448
  private constructor(private readonly host: HostBridgeApi) {}
1272
1449
 
@@ -1300,8 +1477,14 @@ class QtWebChannelAdapter implements BridgeAdapter {
1300
1477
  }
1301
1478
 
1302
1479
  async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
1303
- return new Promise<T>((resolve) => {
1304
- this.host.anQstBridge_call(service, member, args, (result) => resolve(result as T));
1480
+ return new Promise<T>((resolve, reject) => {
1481
+ this.host.anQstBridge_call(service, member, args, (result) => {
1482
+ if (isBridgeCallError(result)) {
1483
+ reject(result);
1484
+ return;
1485
+ }
1486
+ resolve(result as T);
1487
+ });
1305
1488
  });
1306
1489
  }
1307
1490
 
@@ -1331,7 +1514,13 @@ class QtWebChannelAdapter implements BridgeAdapter {
1331
1514
  }
1332
1515
 
1333
1516
  class WebSocketBridgeAdapter implements BridgeAdapter {
1334
- private readonly pending = new Map<string, (result: unknown) => void>();
1517
+ private readonly pending = new Map<string, {
1518
+ service: string;
1519
+ member: string;
1520
+ requestId: string;
1521
+ resolve: (result: unknown) => void;
1522
+ reject: (error: unknown) => void;
1523
+ }>();
1335
1524
  private readonly outputListeners: OutputListener[] = [];
1336
1525
  private readonly slotListeners: SlotInvocationListener[] = [];
1337
1526
  private requestCounter = 0;
@@ -1343,10 +1532,15 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1343
1532
  const type = String(message["type"] ?? "");
1344
1533
  if (type === "callResult") {
1345
1534
  const requestId = String(message["requestId"] ?? "");
1346
- const resolver = this.pending.get(requestId);
1347
- if (resolver) {
1535
+ const pending = this.pending.get(requestId);
1536
+ if (pending) {
1348
1537
  this.pending.delete(requestId);
1349
- resolver(message["result"]);
1538
+ const result = message["result"];
1539
+ if (isBridgeCallError(result)) {
1540
+ pending.reject(result);
1541
+ return;
1542
+ }
1543
+ pending.resolve(result);
1350
1544
  }
1351
1545
  return;
1352
1546
  }
@@ -1377,6 +1571,18 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1377
1571
  this.socket.close();
1378
1572
  }
1379
1573
  });
1574
+ this.socket.addEventListener("close", () => {
1575
+ for (const pending of this.pending.values()) {
1576
+ pending.reject({
1577
+ code: "BridgeDisconnectedError",
1578
+ message: "Bridge disconnected before call completion.",
1579
+ service: pending.service,
1580
+ member: pending.member,
1581
+ requestId: pending.requestId
1582
+ });
1583
+ }
1584
+ this.pending.clear();
1585
+ });
1380
1586
  }
1381
1587
 
1382
1588
  static async create(): Promise<WebSocketBridgeAdapter> {
@@ -1408,8 +1614,14 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1408
1614
  async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
1409
1615
  const requestId = \`req-\${++this.requestCounter}\`;
1410
1616
  const payload = { type: "call", requestId, service, member, args };
1411
- return await new Promise<T>((resolve) => {
1412
- this.pending.set(requestId, (value) => resolve(value as T));
1617
+ return await new Promise<T>((resolve, reject) => {
1618
+ this.pending.set(requestId, {
1619
+ service,
1620
+ member,
1621
+ requestId,
1622
+ resolve: (value) => resolve(value as T),
1623
+ reject
1624
+ });
1413
1625
  this.socket.send(JSON.stringify(payload));
1414
1626
  });
1415
1627
  }
@@ -1520,7 +1732,7 @@ class AnQstBridgeRuntime {
1520
1732
  outputHandler(value);
1521
1733
  }
1522
1734
  });
1523
- this.adapter.onSlotInvocation((requestId, service, member, args) => {
1735
+ this.adapter.onSlotInvocation(async (requestId, service, member, args) => {
1524
1736
  const key = this.key(service, member);
1525
1737
  const handler = this.slotHandlers.get(key);
1526
1738
  if (handler === undefined) {
@@ -1528,10 +1740,15 @@ class AnQstBridgeRuntime {
1528
1740
  return;
1529
1741
  }
1530
1742
  try {
1531
- const result = handler(...args);
1743
+ const result = await Promise.resolve(handler(...args));
1744
+ if (result instanceof Error) {
1745
+ this.adapter!.resolveSlot(requestId, false, undefined, result.message);
1746
+ return;
1747
+ }
1532
1748
  this.adapter!.resolveSlot(requestId, true, result, "");
1533
1749
  } catch (error) {
1534
- this.adapter!.resolveSlot(requestId, false, undefined, String(error));
1750
+ const message = error instanceof Error ? error.message : String(error);
1751
+ this.adapter!.resolveSlot(requestId, false, undefined, message);
1535
1752
  }
1536
1753
  });
1537
1754
  for (const key of this.slotHandlers.keys()) {
@@ -1764,7 +1981,7 @@ function renderNodeExpressWsIndex(spec) {
1764
1981
  sendJson(session.socket, {
1765
1982
  type: "callResult",
1766
1983
  requestId,
1767
- result: { __anqstError: { code: "HandlerNotRegisteredError", message: err.message, service, member } }
1984
+ result: { code: "HandlerNotRegisteredError", message: err.message, service, member, requestId }
1768
1985
  });
1769
1986
  throw err;
1770
1987
  }
@@ -1786,7 +2003,7 @@ function renderNodeExpressWsIndex(spec) {
1786
2003
  sendJson(session.socket, {
1787
2004
  type: "callResult",
1788
2005
  requestId,
1789
- result: { __anqstError: { code: "CallHandlerError", message, service, member } }
2006
+ result: { code: "CallHandlerError", message, service, member, requestId }
1790
2007
  });
1791
2008
  });
1792
2009
  return;
@@ -2138,7 +2355,7 @@ export interface ${spec.widgetName}NodeBridge {
2138
2355
  export function create${spec.widgetName}NodeExpressWsBridge(options: ${spec.widgetName}NodeBridgeOptions): ${spec.widgetName}NodeBridge {
2139
2356
  const wsPath = options.wsPath ?? "/anqst-bridge";
2140
2357
  const devConfigPath = options.devConfigPath ?? "/anqst-dev-config.json";
2141
- const defaultSlotTimeoutMs = options.defaultSlotTimeoutMs ?? 120000;
2358
+ const defaultSlotTimeoutMs = options.defaultSlotTimeoutMs ?? 1000;
2142
2359
  const maxQueuedPerSlot = options.maxQueuedSlotInvocationsPerSlot ?? 1024;
2143
2360
  const sessions = new Map<WebSocket, ${spec.widgetName}NodeSession>();
2144
2361
  const diagnosticListeners = new Set<(diagnostic: AnQstDiagnostic) => void>();
@@ -2230,7 +2447,7 @@ ${callDispatch}
2230
2447
  sendJson(session.socket, {
2231
2448
  type: "callResult",
2232
2449
  requestId,
2233
- result: { __anqstError: { code: "HandlerNotRegisteredError", message: err.message, service, member } }
2450
+ result: { code: "HandlerNotRegisteredError", message: err.message, service, member, requestId }
2234
2451
  });
2235
2452
  throw err;
2236
2453
  }
@@ -2363,7 +2580,8 @@ function generateOutputs(spec, options = { emitQWidget: true, emitAngularService
2363
2580
  const cppTypes = buildCppTypeContext(spec);
2364
2581
  outputs[`${cppDir}/CMakeLists.txt`] = renderCMake(spec);
2365
2582
  outputs[`${cppDir}/${spec.widgetName}.qrc`] = renderEmbeddedQrc(spec.widgetName, []);
2366
- outputs[`${cppDir}/include/${spec.widgetName}.h`] = renderWidgetHeader(spec, cppTypes);
2583
+ outputs[`${cppDir}/include/${spec.widgetName}.h`] = renderWidgetUmbrellaHeader(spec);
2584
+ outputs[`${cppDir}/include/${spec.widgetName}Widget.h`] = renderWidgetHeader(spec, cppTypes);
2367
2585
  outputs[`${cppDir}/include/${spec.widgetName}Types.h`] = renderTypesHeader(spec, cppTypes);
2368
2586
  outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes);
2369
2587
  }
@@ -2579,6 +2797,7 @@ add_custom_command(
2579
2797
  "\${${generatedRootVar}}/${widgetName}.qrc"
2580
2798
  "\${${generatedRootVar}}/${widgetName}.cpp"
2581
2799
  "\${${generatedIncludeVar}}/${widgetName}.h"
2800
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2582
2801
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2583
2802
  "\${${generatedRootVar}}/webapp/index.html"
2584
2803
  COMMAND "\${ANQST_NPM_EXECUTABLE}" install
@@ -2594,6 +2813,7 @@ add_custom_target(${autogenTarget}
2594
2813
  "\${${generatedRootVar}}/${widgetName}.qrc"
2595
2814
  "\${${generatedRootVar}}/${widgetName}.cpp"
2596
2815
  "\${${generatedIncludeVar}}/${widgetName}.h"
2816
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2597
2817
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2598
2818
  "\${${generatedRootVar}}/webapp/index.html"
2599
2819
  )
@@ -2602,6 +2822,7 @@ set_source_files_properties(
2602
2822
  "\${${generatedRootVar}}/${widgetName}.qrc"
2603
2823
  "\${${generatedRootVar}}/${widgetName}.cpp"
2604
2824
  "\${${generatedIncludeVar}}/${widgetName}.h"
2825
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2605
2826
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2606
2827
  PROPERTIES GENERATED TRUE
2607
2828
  )
@@ -2610,6 +2831,7 @@ add_library(${widgetTarget}
2610
2831
  "\${${generatedRootVar}}/${widgetName}.qrc"
2611
2832
  "\${${generatedRootVar}}/${widgetName}.cpp"
2612
2833
  "\${${generatedIncludeVar}}/${widgetName}.h"
2834
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2613
2835
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2614
2836
  )
2615
2837
  add_dependencies(${widgetTarget} ${autogenTarget})
@@ -2797,7 +3019,7 @@ function installDesignerPluginIconAssets(cwd, pluginDir) {
2797
3019
  }
2798
3020
  function renderQtDesignerPluginCpp(widgetName, widgetCategory, hasIcon) {
2799
3021
  const pluginClass = `${widgetName}DesignerPlugin`;
2800
- const widgetClass = `${widgetName}::${widgetName}`;
3022
+ const widgetClass = `${widgetName}Widget`;
2801
3023
  const groupName = escapeCppStringLiteral(widgetCategory);
2802
3024
  const iconExpression = hasIcon
2803
3025
  ? 'QIcon(QStringLiteral(":/anqstdesignerplugin/plugin-icon.png"))'