mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-30 21:41:10 +00:00
#40 made lp_invoke_async able to report a failure, but only for the two conditions produced ABOVE the transport: acquire failure and the unauthorized sentinel. Everything the transport learns while the call is in flight was still discarded — PlainLogosObject answered a bare QVariant() for a timeout and for `ResultMessage.ok == false` alike, and LogosAPIConsumer hard-coded an empty CallError next to it. Two ordinary failures therefore still reported success on both entry points: a TIMEOUT, and MODULE NOT LOADED against a host that is up (which is not an acquire failure on the plain wire — requestObject hands back a handle for any name over an open connection). The information already exists: ResultMessage carries err/errCode, the futures know they expired, QtRO knows its pending call never finished. It had nowhere to go because LogosObject's callMethod returns a lone QVariant and its callMethodAsync callback takes a lone QVariant. Widening those virtuals would append a vtable slot to an installed, subclassed interface, so instead this adds LogosObjectErrorChannel — a SIBLING interface reached by dynamic_cast. LogosObject's size, layout and vtable are unchanged (verified: a subclass compiled against the old and new headers emits the same 14-entry vtable with identical slot indices), and a transport that does not implement it keeps today's behaviour. logos_protocol.cpp needs no change: lp_invoke and lp_invoke_async already render this CallError, so both twins gain the coverage together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
4.4 KiB
C++
100 lines
4.4 KiB
C++
#ifndef LOGOS_CALL_ERROR_H
|
|
#define LOGOS_CALL_ERROR_H
|
|
|
|
// The canonical cross-module call error — the C++ face of the protocol's
|
|
// {code, message, origin} error JSON (see makeErrorJson / lp_invoke's
|
|
// out_error_json). Deliberately Qt-free: it crosses into Qt-free module
|
|
// code (generated typed wrappers expose it as an optional out-parameter,
|
|
// e.g. `calc.add(a, b, &err)`), so only std types appear here.
|
|
|
|
#include <string>
|
|
|
|
namespace logos {
|
|
|
|
// Error codes are lowercase snake_case strings, mirroring the C ABI's JSON
|
|
// contract rather than an enum so the set can grow (transport-level codes,
|
|
// provider dispatch errors) without an ABI break.
|
|
//
|
|
// Currently produced:
|
|
// "object_unavailable" — the target module/object is not there: it could not
|
|
// be acquired, or the transport answered that it is
|
|
// not published. One code for one condition, whether
|
|
// it is detected at acquire time (QtRO, which resolves
|
|
// a replica up front) or at call time (the plain wire,
|
|
// whose requestObject hands back a handle for any name
|
|
// over an open connection and only learns the truth
|
|
// from the reply).
|
|
// "timeout" — the caller's deadline elapsed with no reply.
|
|
// "transport_error" — the connection failed or was torn down mid-call.
|
|
// "call_failed" — the peer could not dispatch the call at all (as
|
|
// distinct from a provider that ran and REJECTED it,
|
|
// which answers the "dispatch_failed" envelope as its
|
|
// result value — see logos-cpp-sdk#129).
|
|
// "unauthorized" — the provider rejected our token and the one
|
|
// permitted re-exchange also failed.
|
|
struct CallError {
|
|
std::string code; // empty = no error
|
|
std::string message;
|
|
std::string origin; // module the error originated from / was detected for
|
|
|
|
bool ok() const { return code.empty(); }
|
|
void clear() { code.clear(); message.clear(); origin.clear(); }
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Canonical constructors.
|
|
//
|
|
// Every transport that can detect one of these produces it HERE rather than
|
|
// spelling the code out at the failure site, so a caller decoding a
|
|
// {code, message, origin} object gets the same vocabulary no matter which wire
|
|
// the call went over — and so lp_invoke and lp_invoke_async, which both render
|
|
// this struct with the same makeErrorJson(), stay indistinguishable.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
inline CallError callErrorTimeout(const std::string& origin,
|
|
const std::string& method, int timeoutMs)
|
|
{
|
|
return {"timeout",
|
|
"call to '" + origin + "." + method + "' timed out after "
|
|
+ std::to_string(timeoutMs) + "ms",
|
|
origin};
|
|
}
|
|
|
|
inline CallError callErrorObjectUnavailable(const std::string& origin,
|
|
const std::string& detail)
|
|
{
|
|
return {"object_unavailable", detail, origin};
|
|
}
|
|
|
|
inline CallError callErrorTransport(const std::string& origin,
|
|
const std::string& detail)
|
|
{
|
|
return {"transport_error", detail, origin};
|
|
}
|
|
|
|
inline CallError callErrorCallFailed(const std::string& origin,
|
|
const std::string& detail)
|
|
{
|
|
return {"call_failed", detail, origin};
|
|
}
|
|
|
|
// Map a plain-wire ResultMessage failure (errCode + err) onto the vocabulary
|
|
// above. The wire's codes are the transport's own spelling; this is the single
|
|
// place they are translated, so a new wire code degrades to "call_failed"
|
|
// instead of silently becoming an empty CallError (which reads as SUCCESS).
|
|
inline CallError callErrorFromWire(const std::string& origin,
|
|
const std::string& wireCode,
|
|
const std::string& message)
|
|
{
|
|
const std::string detail = message.empty() ? wireCode : message;
|
|
if (wireCode == "MODULE_NOT_LOADED")
|
|
return callErrorObjectUnavailable(origin, detail);
|
|
if (wireCode == "TRANSPORT_CLOSED" || wireCode == "TRANSPORT_ERROR")
|
|
return callErrorTransport(origin, detail);
|
|
return callErrorCallFailed(origin, detail);
|
|
}
|
|
|
|
} // namespace logos
|
|
|
|
#endif // LOGOS_CALL_ERROR_H
|