Files
logos-protocol/cpp/logos_api_client.h
Dario LipicarandClaude Opus 5 03842db5c1 feat(windows): named pipes, an explicit lp_* ABI, and a cross target (#58)
* feat(windows): port logos_socket_paths and add a cross target

logos_socket_paths.cpp is the only POSIX-bound file in logos-protocol. All of
it is unix-domain-socket machinery, and on Windows the local transport is named
pipes (QLocalServer maps a name to \\.\pipe\<name>), where none of the
assumptions hold: a pipe has no inode to lstat/chown/chmod -- access comes from
a security descriptor set at CreateNamedPipe time -- and a pipe cannot outlive
its last handle, so a hard-killed process leaves nothing behind.

isSocketDead and reapStaleSockets are therefore not merely unimplemented on
Windows, they are vacuous: the state they detect cannot arise. Both return the
fail-closed answer (false / 0), matching the documented contract that an
endpoint is never reported dead unless certain.

applySocketPerms deliberately does NOT no-op. With no policy requested it
returns true, as on POSIX. But when LOGOS_SOCKET_GROUP or LOGOS_SOCKET_MODE
*are* set it fails with an explanatory error, because silently returning true
would leave the endpoint more permissive than the operator asked for -- the one
direction this file is careful never to go (cf. the chgrp-then-chmod ordering
in the POSIX branch). Granting a pipe to a group needs a DACL plus a
group->SID resolver; until that exists, refuse loudly.

Also gates qt6.wrapQtAppsNoGuiHook behind !isWindows and sets dontWrapQtApps.
Both halves are required: the hook does not even evaluate for a mingw host, it
would be inert anyway (wrap-qt-apps-hook.sh skips anything that is not ELF or
Mach-O), and qtbase's setup hook hard-errors in qtPreHook unless
dontWrapQtApps is set.

Header contract updated per function. POSIX branch unchanged and still compiles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: make the Boost.System component optional, not required

find_package(Boost REQUIRED COMPONENTS system) hard-fails on Boost 1.89:

    Could not find a package configuration file provided by "boost_system"

Boost.System has been header-only for years, and 1.89 finally dropped the
compiled boost_system library, so no boost_systemConfig.cmake is installed at
all. The COMPONENTS request was not gratuitous though -- on 1.87 the
Boost::system imported target is only exported when the component is asked
for, which is what the previous comment recorded.

So ask optionally and fall back to Boost::headers, which supplies the same
header-only error_code either way. The choice is by BOOST VERSION, not by
platform: this is not a Windows quirk, it simply surfaced first there because
the Windows target pins a newer nixpkgs (Boost 1.89) than the native one
(Boost 1.87).

Verified both ways -- native aarch64-darwin still selects Boost::system:
    -- Boost.System target: Boost::system (Boost 1.87.0)
and the build completes unchanged.

Also adds QT_HOST_PATH / QT_ADDITIONAL_HOST_PACKAGES_PREFIX_PATH for the
Windows target. Qt6RemoteObjectsDependencies.cmake declares
    set(__qt_RemoteObjects_tool_deps "Qt6RemoteObjectsTools;6.11.1")
and Qt6RemoteObjectsTools holds repc, which must RUN on the build machine --
so under cross it lives in the build-platform Qt, not the mingw one. Without
these, find_package reports the thoroughly misleading "Expected Config file at
<qtbase>/lib/cmake/Qt6RemoteObjects ... does NOT exist": the TARGET config is
found fine; it is the HOST tool package that is missing. Every Qt-consuming
repo will need this, so it should be hoisted into logos-nix rather than
repeated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: declare the lp_* C ABI explicitly instead of relying on auto-export

Adds LP_API (__declspec(dllexport) when building the shared library, default
visibility elsewhere) to the 21 lp_* entry points, and defines
LOGOS_PROTOCOL_BUILDING_SHARED for the shared target only, so the static
archive leaves LP_API empty and its consumers need no import library.

This is NOT a bug fix, contrary to what the concern in the Windows plan
suggested. Measured on the cross-built DLL, before and after:

    before:  export table 0x2ece (11982 symbols), lp_* present: 21
    after:   export table 0x15   (   21 symbols), lp_* present: 21

GNU ld's PE auto-export was already exporting lp_* -- along with roughly
twelve thousand other symbols. The worry was that logos_module_impl.h's
__declspec(dllexport) would disable auto-export image-wide and silently drop
lp_*; it does not, because no translation unit in logos_protocol includes that
header (it is listed in PROTOCOL_SOURCES for IDE visibility only).

What this does buy is worth having anyway: the exported surface is now the ABI
we actually declare rather than whatever happens to have external linkage, it
stops being contingent on auto-export staying enabled -- which the very next
TU to gain a dllexport would silently end -- and it drops ~12k incidental
symbols from the export table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: relax the Boost.System requirement in the EXPORTED cmake config too

The previous commit fixed cpp/CMakeLists.txt but left
logos-protocolConfig.cmake.in still doing

    find_dependency(Boost REQUIRED COMPONENTS system)

so logos-protocol itself built fine on Boost 1.89 while every CONSUMER of its
installed CMake package failed at configure time -- caught by logos-qt-sdk,
which is the first downstream repo to be cross-built.

Worth noting as a general trap: a package can be internally consistent and
still ship a broken contract, because the exported config is a separate
artifact from the build. Anything changed in one has to be checked in the
other.

Verified both directions: the Windows cross builds of logos-cpp-sdk and
logos-qt-sdk now succeed, and a native aarch64-darwin logos-qt-sdk build --
which consumes this same config against Boost 1.87 -- still succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(windows): mark the types that must exist once per process

PE has no symbol interposition. ELF and Mach-O interpose across the whole
image set, so when liblogos_core exports TokenManager::instance() every
other image binds to that one definition and the function-local
`static TokenManager instance;` is genuinely a singleton. On Windows
every image that links liblogos_protocol.a / liblogos_qt_sdk.a statically
gets its own copy of the code and therefore its own statics -- measured:
NINE images in the Basecamp payload each defined
TokenManager::instance()::instance. The host saved a capability token
into its copy, the UI plugin read its own empty copy, and every
cross-module call was refused (29 "ModuleProxy: rejecting unauthorized
call").

LOGOS_SHARED_API marks the affected types. It expands to
__declspec(dllimport) only for a consumer that opts in with
LOGOS_SHARED_USE_DLL, and to nothing everywhere else -- off Windows, and
inside logos-protocol/logos-qt-sdk/liblogos_core themselves, so the
static archives compile byte-identically to before.

The dllimport is the load-bearing half, not the export: it rewrites the
reference to go through __imp_, so the plain symbol is never undefined
and GNU ld never pulls the archive member that would redefine it. Without
it the link still succeeds, binds to the archive, and gives no diagnostic
at all.

logos_shared_api.h records both wrong answers -- export everything
(collides with the static archive over LogosAPI) and export nothing
(today's silent per-image statics) -- so neither gets reinvented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): let checks and devShells take the arg forAllSystems now passes

The cross-target commit added `inherit system;` to forAllSystems so the Windows
arm could tell which target it was building, but left `checks` and `devShells`
on the strict `({ pkgs }: ...)` pattern.  A Nix attrset pattern without `...` is
exact, so both stopped evaluating:

    error: function 'anonymous lambda' called with unexpected argument 'system'

on EVERY platform, not just Windows -- `nix flake check` and `ws develop
logos-protocol` are dead on this branch while they work on master.  `packages`
was unaffected because it goes through forAllTargets, which is why nothing
caught it.

Measured, same worktree, before and after:
  before: checks.aarch64-darwin -> the error above at flake.nix:52
  after:  checks.aarch64-darwin -> [ "tests" ]
          devShells.aarch64-darwin.default.name -> "nix-shell"
          packages -> [ aarch64-darwin aarch64-linux x86_64-darwin x86_64-linux
                        x86_64-windows ]

* chore(deps): re-pin logos-nix to the merged Windows overlay

The cross overlay landed in logos-nix#2.  This branch was locked to a
pre-merge rev, which has no `lib.forAllTargets` and no `lib.mkWindowsPkgs`,
so it could not evaluate standalone -- only against the unmerged branch.

Level 2 of the Windows chain; L1 (logos-nix) is merged.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:44:25 -03:00

416 lines
22 KiB
C++

#ifndef LOGOS_API_CLIENT_H
#define LOGOS_API_CLIENT_H
#include <QObject>
#include <QString>
#include <QVariant>
#include <QVariantList>
#include <QMap>
#include <QStringList>
#include <functional>
#include <string>
#include <vector>
#include "logos_call_error.h"
#include "logos_mode.h"
#include "logos_shared_api.h"
#include "logos_subscription_state.h"
#include "logos_transport_config.h"
#include <nlohmann/json.hpp>
class LogosAPI;
class LogosAPIConsumer;
class LogosObject;
class TokenManager;
/**
* @brief LogosAPIClient provides a high-level interface for remote method calls
*
* This class serves as a facade over LogosAPIConsumer, providing a clean interface
* for applications that need to call remote methods and handle events.
*
* LOGOS_SHARED_API for the same reason as TokenManager, plus one of its own:
* this is the object that reaches the shared TokenManager on the call path, and
* archives are pulled a whole OBJECT FILE at a time. If the consumer imports
* TokenManager but not LogosAPIClient, ld pulls logos_api_client.cpp.obj out of
* liblogos_protocol.a for the client, that object drags token_manager.cpp.obj
* back in with it, and the duplicate static reappears. See logos_shared_api.h.
*/
class LOGOS_SHARED_API LogosAPIClient : public QObject
{
Q_OBJECT
public:
/**
* @brief Construct a client with explicit transports for both the
* target module *and* `capability_module`.
*
* Two transports because the SDK's auto-`requestModule` flow inside
* invokeRemoteMethod{,Async} dials `capability_module` to fetch a
* per-target token. When the daemon advertises capability_module on
* a different transport from the target (e.g. CLI on host →
* core_service over TCP, but capability_module also over TCP on a
* sibling port), the auto-dial must use the right one. Pre-building
* the consumer once in the constructor (see m_capability_consumer)
* keeps the hot path free of per-call lookups.
*/
LogosAPIClient(const QString& module_to_talk_to,
const QString& origin_module,
TokenManager* token_manager,
const LogosTransportConfig& target_transport,
const LogosTransportConfig& capability_transport,
QObject *parent = nullptr);
/**
* @brief No-transport constructor — both target and
* capability_module use the process-global default
* (LocalSocket) via LogosTransportConfigGlobal::getDefault().
*/
explicit LogosAPIClient(const QString& module_to_talk_to,
const QString& origin_module,
TokenManager* token_manager,
QObject *parent = nullptr);
~LogosAPIClient();
/**
* @brief Request a LogosObject handle by name
* @return LogosObject* handle, or nullptr if failed
*/
LogosObject* requestObject(const QString& objectName, Timeout timeout = Timeout());
bool isConnected() const;
QString registryUrl() const;
bool reconnect();
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
const QVariantList& args = QVariantList(), Timeout timeout = Timeout());
/**
* @brief invokeRemoteMethod with an explicit error out-channel.
*
* Fills *err with the canonical {code, message, origin} call error when
* the failure is detectable (today: "object_unavailable" when the target
* object cannot be acquired); cleared on success. Generated typed client
* wrappers call this overload and throw logos::LogosCallError so callers
* can distinguish a failed call from a legitimately default-valued
* result.
*/
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
const QVariantList& args, Timeout timeout, logos::CallError* err);
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
const QVariant& arg, Timeout timeout = Timeout());
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout());
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout());
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, Timeout timeout = Timeout());
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout());
// const char* overloads — resolve ambiguity for string literals
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
const QVariantList& args = QVariantList(), Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString(objectName), QString(methodName), args, timeout); }
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
const QVariant& arg, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg, timeout); }
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, timeout); }
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, timeout); }
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, arg4, timeout); }
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, arg4, arg5, timeout); }
// std::string overloads — thin wrappers that convert internally
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
const QVariantList& args = QVariantList(), Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), args, timeout); }
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
const QVariant& arg, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg, timeout); }
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, timeout); }
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, timeout); }
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, arg4, timeout); }
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout())
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, arg4, arg5, timeout); }
// const char* onEvent overload
void onEvent(LogosObject* originObject, const char* eventName,
std::function<void(const QString&, const QVariantList&)> callback)
{ onEvent(originObject, QString(eventName), callback); }
// std::string onEvent overloads
void onEvent(LogosObject* originObject, const std::string& eventName,
std::function<void(const QString&, const QVariantList&)> callback)
{ onEvent(originObject, QString::fromStdString(eventName), callback); }
void onEvent(LogosObject* originObject, const std::string& eventName,
std::function<void(const std::string&, const QVariantList&)> callback)
{
onEvent(originObject, QString::fromStdString(eventName),
[cb = std::move(callback)](const QString& name, const QVariantList& args) {
cb(name.toStdString(), args);
});
}
void onEvent(LogosObject* originObject, const char* eventName,
std::function<void(const std::string&, const QVariantList&)> callback)
{
onEvent(originObject, QString(eventName),
[cb = std::move(callback)](const QString& name, const QVariantList& args) {
cb(name.toStdString(), args);
});
}
using AsyncResultCallback = std::function<void(QVariant)>;
/**
* @brief Async callback with an explicit error out-channel.
*
* Mirrors the sync `invokeRemoteMethod(..., CallError*)` overload. Set to
* code="object_unavailable" when the target object cannot be acquired,
* cleared on success. Callers that need to distinguish acquire failure
* from a legitimately empty QVariant result should use this overload.
*/
using AsyncResultErrorCallback = std::function<void(QVariant, const logos::CallError&)>;
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
const QVariantList& args, AsyncResultCallback callback,
Timeout timeout = Timeout());
/**
* @brief invokeRemoteMethodAsync with an explicit error out-channel.
* See AsyncResultErrorCallback docs above.
*/
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
const QVariantList& args, AsyncResultErrorCallback callback,
Timeout timeout = Timeout());
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
const QVariant& arg, AsyncResultCallback callback,
Timeout timeout = Timeout());
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2,
AsyncResultCallback callback, Timeout timeout = Timeout());
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
AsyncResultCallback callback, Timeout timeout = Timeout());
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, AsyncResultCallback callback,
Timeout timeout = Timeout());
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
const QVariant& arg4, const QVariant& arg5,
AsyncResultCallback callback, Timeout timeout = Timeout());
/**
* @brief Register an event listener via LogosObject's callback mechanism
* @param originObject The LogosObject that will emit the event
* @param eventName The name of the event to listen for
* @param callback Function to call when the event is triggered
*/
void onEvent(LogosObject* originObject, const QString& eventName,
std::function<void(const QString&, const QVariantList&)> callback);
/**
* @brief Subscribe to an event on a module that may not be reachable YET.
*
* The safe alternative to requestObject() + onEvent() for any caller that
* subscribes during startup — a UI plugin's Component.onCompleted, a
* module's initLogos(), a backend's onContextReady(). Never blocks; arms
* when the module appears, including a mid-session install; warns once on
* deferral and logs when it arms. See LogosAPIConsumer::onEventWhenAvailable
* for the full contract, the cost of waiting, and the (deliberate) lack of
* de-duplication.
*
* Adds no member state to this class — see the ABI note below; it forwards
* to the consumer that already exists.
*
* @return A non-zero id for cancelEventSubscription() /
* eventSubscriptionState(), or 0 if the arguments were refused.
*/
quint64 onEventWhenAvailable(const QString& objectName, const QString& eventName,
std::function<void(const QString&, const QVariantList&)> callback,
std::function<void(bool)> onArmed = {});
quint64 onEventWhenAvailable(const std::string& objectName, const std::string& eventName,
std::function<void(const std::string&, const QVariantList&)> callback,
std::function<void(bool)> onArmed = {})
{
return onEventWhenAvailable(QString::fromStdString(objectName),
QString::fromStdString(eventName),
[cb = std::move(callback)](const QString& name, const QVariantList& args) {
cb(name.toStdString(), args);
},
std::move(onArmed));
}
/**
* @brief Call `onReady` once, as soon as the module becomes acquirable.
*
* The call-path counterpart of onEventWhenAvailable(), for a caller that
* must invoke a method on a module which may still be starting: it neither
* fails fast (stranding a UI that will never retry) nor calls straight
* through into the transport's acquire timeout on the calling thread. Fires
* exactly once, is not re-armed on reconnect, and holds no subscription.
* See LogosAPIConsumer::whenObjectAvailable for the full contract.
*
* @return A non-zero id accepted by cancelEventSubscription(), or 0.
*/
quint64 whenObjectAvailable(const QString& objectName,
std::function<void(bool)> onReady);
/**
* @brief Stop tracking the subscription with this id.
*
* See LogosAPIConsumer::cancelEventSubscription — in particular that it
* stops the BOOKKEEPING (the retry timer, the watchdog, the reconnect
* re-arm set) and does not detach the callback from the shared handle.
*/
bool cancelEventSubscription(quint64 subscriptionId);
/**
* @brief Whether a subscription id is still pending, armed, or forgotten.
* For callers that keep their own de-duplication record and need to
* check it rather than trust it.
*/
LogosSubscriptionState eventSubscriptionState(quint64 subscriptionId) const;
/**
* @brief Diagnostics: "<object>::<event>" for every deferred subscription
* that has not armed yet.
*/
QStringList pendingEventSubscriptions() const;
/**
* @brief Emit an event on a LogosObject (for plugins that act as event sources)
* @param object The LogosObject to emit the event on
* @param eventName The name of the event
* @param data The event data
*/
void onEventResponse(LogosObject* object, const QString& eventName, const QVariantList& data);
/**
* @brief Backward-compatible overload for QObject-based plugins.
*
* Old-API plugins call onEventResponse(this, ...) where `this` is a QObject*.
* This overload invokes the eventResponse signal on the QObject via QMetaObject.
*/
void onEventResponse(QObject* object, const QString& eventName, const QVariantList& data);
bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token);
bool informModuleToken(const char* authToken, const char* moduleName, const char* token)
{ return informModuleToken(QString(authToken), QString(moduleName), QString(token)); }
bool informModuleToken(const std::string& authToken, const std::string& moduleName, const std::string& token);
bool informModuleToken_module(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token, int timeoutMs = 20000);
TokenManager* getTokenManager() const;
QString getToken(const QString& module_name);
// nlohmann::json overloads — args is a JSON array, result is a JSON value.
// These convert between nlohmann::json and QVariant internally so callers
// never need to touch Qt JSON types.
nlohmann::json invokeRemoteMethod(const std::string& objectName,
const std::string& methodName,
const nlohmann::json& args,
Timeout timeout = Timeout());
// nlohmann::json event callback overload — data arrives as a json array.
void onEvent(LogosObject* originObject, const std::string& eventName,
std::function<void(const std::string&, const nlohmann::json&)> callback);
private:
// requestModule handshake against capability_module + cache the minted token
// in the shared TokenManager. Returns the token ("" on failure). Factors out
// the first-exchange logic so both the initial fetch and the
// rejection-driven re-exchange share one path. (Private method — no effect on
// the ABI-sensitive data layout below.)
// `timeout` is the CALLER's budget, and it bounds the handshake too. It has
// to: the exchange runs before the call on an un-tokened target, so a bound
// that skipped it would describe only the second half of the operation.
QString mintAndCacheToken(const QString& objectName, Timeout timeout);
// Async invoke with a bounded retry budget backing the public
// invokeRemoteMethodAsync overloads. On a provider rejection sentinel it
// drops the stale token and re-enters itself with retriesLeft-1, so the retry
// coalesces through the same m_pendingHandshakes machinery.
void invokeRemoteMethodAsyncImpl(const QString& objectName, const QString& methodName,
const QVariantList& args, AsyncResultErrorCallback callback,
Timeout timeout, int retriesLeft);
// ABI note: this private layout is consumed by every plugin that
// statically links libsdk. Adding a new field in the middle of
// this section shifts the offsets of subsequent fields and
// SILENTLY breaks any plugin compiled before the change — it
// reads m_token_manager at the wrong offset and segfaults on
// the first cross-process call. New private members MUST be
// appended to the end. (Long-term cure: pimpl this class so
// sizeof / offsets become opaque to consumers.)
LogosAPIConsumer* m_consumer;
QMap<QString, QString> m_tokens;
TokenManager* m_token_manager;
QString m_origin_module;
// Pre-built consumer for the auto-`requestModule` token-fetch path
// in invokeRemoteMethod{,Async}. Constructed once with the right
// transport (see the two-transport ctor) so the hot path doesn't
// chase a back-pointer to LogosAPI just to look up the transport
// registry. Null only when `m_consumer` itself is for
// capability_module (no recursion). In-class default to nullptr
// so any old constructor that doesn't list this field still
// leaves a defined value.
LogosAPIConsumer* m_capability_consumer = nullptr;
// Per-target queue of continuations waiting on an in-flight async
// requestModule handshake. The FIRST async call to an un-tokened target
// starts exactly one handshake; concurrent calls to the same target queue
// here and all drain with the single minted token when it resolves. Without
// this coalescing a fan-out of N first-calls fires N racing handshakes whose
// distinct tokens overwrite each other on the target — so already-dispatched
// calls carry a superseded token and get rejected. Touched only on the
// owner thread (invokeRemoteMethodAsync marshals there), so it needs no
// lock. Appended last per the ABI note above; defaults to empty.
QMap<QString, std::vector<std::function<void(const QString&)>>> m_pendingHandshakes;
};
#endif // LOGOS_API_CLIENT_H