#ifndef LOGOS_PLAIN_RPC_CONNECTION_H #define LOGOS_PLAIN_RPC_CONNECTION_H #include "incoming_call_handler.h" #include "rpc_framing.h" #include "rpc_message.h" #include "wire_codec.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace logos::plain { // ----------------------------------------------------------------------------- // RpcConnectionBase — type-erased public surface of RpcConnection. // // Callers (plain_logos_object, plain_transport_host) hold a // shared_ptr so they don't have to know whether the // underlying socket is plain TCP or TLS-wrapped TCP. All the async machinery // lives in the templated subclass. // ----------------------------------------------------------------------------- class RpcConnectionBase { public: using ErrorHandler = std::function; virtual ~RpcConnectionBase() = default; virtual void start() = 0; virtual void stop(const std::string& reason = "stopped") = 0; virtual bool isOpen() const = 0; virtual std::future sendCall(CallMessage msg) = 0; virtual std::future sendMethods(MethodsMessage msg) = 0; virtual void sendSubscribe(SubscribeMessage msg, std::function callback) = 0; virtual void sendUnsubscribe(UnsubscribeMessage msg) = 0; virtual void sendEvent(EventMessage msg) = 0; virtual void sendToken(TokenMessage msg) = 0; virtual void setErrorHandler(ErrorHandler handler) = 0; virtual uint64_t nextId() = 0; }; // ----------------------------------------------------------------------------- // RpcConnection — one full-duplex RPC conversation over a Boost.Asio // stream-like socket (plain TCP or SSL-wrapped TCP, sharing this template). // // Roles: the same connection supports both directions. Either peer can // initiate Call / Methods / Subscribe / Token / Event messages. Provider-side // dispatch of inbound Call/Methods/Subscribe/Token goes through an // IncomingCallHandler supplied at construction (may be null for pure-consumer // connections). // // Lifecycle: heap-allocated via std::make_shared; call start() once the // socket is ready; call stop() (or destroy) to tear down. // ----------------------------------------------------------------------------- template class RpcConnection : public RpcConnectionBase , public std::enable_shared_from_this> { public: RpcConnection(Stream stream, std::shared_ptr codec, IncomingCallHandler* handler = nullptr); void start() override; void stop(const std::string& reason = "stopped") override; bool isOpen() const override { return !m_stopped.load(); } std::future sendCall(CallMessage msg) override; std::future sendMethods(MethodsMessage msg) override; void sendSubscribe(SubscribeMessage msg, std::function callback) override; void sendUnsubscribe(UnsubscribeMessage msg) override; void sendEvent(EventMessage msg) override; void sendToken(TokenMessage msg) override; void setErrorHandler(ErrorHandler handler) override { std::lock_guard g(m_mu); m_error = std::move(handler); } uint64_t nextId() override { return m_nextId.fetch_add(1, std::memory_order_relaxed); } private: void doRead(); void handleFrame(MessageType tag, std::vector payload); void dispatchIncoming(AnyMessage msg); void writeFrame(std::vector frame); void doWrite(); void fail(const std::string& reason); Stream m_stream; std::shared_ptr m_codec; IncomingCallHandler* m_handler; boost::asio::strand m_strand; // Read side FrameReader m_reader; std::vector m_readBuf; // Write side std::deque> m_writeQueue; bool m_writing = false; // Outgoing-pending maps std::mutex m_mu; std::map>> m_pendingCalls; std::map>> m_pendingMethods; using EventKey = std::pair; // object, event std::map> m_eventCallbacks; ErrorHandler m_error; std::atomic m_nextId{1}; std::atomic m_stopped{false}; std::atomic m_started{false}; }; // ── Template implementation (must be visible at instantiation sites) ───── template RpcConnection::RpcConnection(Stream stream, std::shared_ptr codec, IncomingCallHandler* handler) : m_stream(std::move(stream)) , m_codec(std::move(codec)) , m_handler(handler) , m_strand(boost::asio::make_strand(m_stream.get_executor())) { m_readBuf.resize(4096); } template void RpcConnection::start() { bool expected = false; if (!m_started.compare_exchange_strong(expected, true)) return; auto self = this->shared_from_this(); boost::asio::post(m_strand, [self] { self->doRead(); }); } template void RpcConnection::stop(const std::string& reason) { fail(reason); } template void RpcConnection::doRead() { auto self = this->shared_from_this(); m_stream.async_read_some(boost::asio::buffer(m_readBuf), boost::asio::bind_executor(m_strand, [self](const boost::system::error_code& ec, std::size_t n) { if (ec) { self->fail(ec.message()); return; } try { self->m_reader.append(self->m_readBuf.data(), n); MessageType tag; std::vector payload; while (self->m_reader.next(tag, payload)) { self->handleFrame(tag, std::move(payload)); } } catch (const std::exception& e) { self->fail(std::string("frame error: ") + e.what()); return; } self->doRead(); })); } template void RpcConnection::handleFrame(MessageType tag, std::vector payload) { AnyMessage msg; try { msg = m_codec->decode(tag, payload.data(), payload.size()); } catch (const std::exception& e) { fail(std::string("decode error: ") + e.what()); return; } dispatchIncoming(std::move(msg)); } template void RpcConnection::dispatchIncoming(AnyMessage msg) { std::visit([this](auto&& m) { using T = std::decay_t; if constexpr (std::is_same_v) { std::shared_ptr> p; { std::lock_guard g(m_mu); auto it = m_pendingCalls.find(m.id); if (it != m_pendingCalls.end()) { p = std::move(it->second); m_pendingCalls.erase(it); } } if (p) p->set_value(std::forward(m)); } else if constexpr (std::is_same_v) { std::shared_ptr> p; { std::lock_guard g(m_mu); auto it = m_pendingMethods.find(m.id); if (it != m_pendingMethods.end()) { p = std::move(it->second); m_pendingMethods.erase(it); } } if (p) p->set_value(std::forward(m)); } else if constexpr (std::is_same_v) { std::function cb; std::function wildcardCb; { std::lock_guard g(m_mu); auto it = m_eventCallbacks.find({m.object, m.eventName}); if (it != m_eventCallbacks.end()) cb = it->second; auto wit = m_eventCallbacks.find({m.object, std::string{}}); if (wit != m_eventCallbacks.end()) wildcardCb = wit->second; } if (cb) cb(m); if (wildcardCb) wildcardCb(m); } else if constexpr (std::is_same_v) { if (!m_handler) return; auto self = this->shared_from_this(); m_handler->onCall(m, [self](ResultMessage res) { self->writeFrame(encodeFrame(*self->m_codec, AnyMessage{std::move(res)})); }); } else if constexpr (std::is_same_v) { if (!m_handler) return; auto self = this->shared_from_this(); m_handler->onMethods(m, [self](MethodsResultMessage res) { self->writeFrame(encodeFrame(*self->m_codec, AnyMessage{std::move(res)})); }); } else if constexpr (std::is_same_v) { if (!m_handler) return; // weak_ptr capture so the host's stored sink doesn't keep the // connection alive past its natural lifetime — without this, // `[self]` would leak every subscribed connection until // unsubscribe (which a crashing client never sends). std::weak_ptr> weak = this->shared_from_this(); const void* connId = static_cast(this); m_handler->onSubscribe(m, [weak](EventMessage evt) { if (auto self = weak.lock()) self->sendEvent(std::move(evt)); }, connId); } else if constexpr (std::is_same_v) { if (m_handler) m_handler->onUnsubscribe(m, static_cast(this)); } else if constexpr (std::is_same_v) { if (m_handler) m_handler->onToken(m); } }, std::move(msg)); } template std::future RpcConnection::sendCall(CallMessage msg) { auto p = std::make_shared>(); auto f = p->get_future(); if (m_stopped.load()) { ResultMessage r; r.id = msg.id; r.ok = false; r.err = "connection stopped"; r.errCode = "TRANSPORT_CLOSED"; p->set_value(std::move(r)); return f; } { std::lock_guard g(m_mu); m_pendingCalls[msg.id] = p; } writeFrame(encodeFrame(*m_codec, AnyMessage{std::move(msg)})); return f; } template std::future RpcConnection::sendMethods(MethodsMessage msg) { auto p = std::make_shared>(); auto f = p->get_future(); if (m_stopped.load()) { MethodsResultMessage r; r.id = msg.id; r.ok = false; r.err = "connection stopped"; p->set_value(std::move(r)); return f; } { std::lock_guard g(m_mu); m_pendingMethods[msg.id] = p; } writeFrame(encodeFrame(*m_codec, AnyMessage{std::move(msg)})); return f; } template void RpcConnection::sendSubscribe(SubscribeMessage msg, std::function cb) { { std::lock_guard g(m_mu); m_eventCallbacks[{msg.object, msg.eventName}] = std::move(cb); } writeFrame(encodeFrame(*m_codec, AnyMessage{std::move(msg)})); } template void RpcConnection::sendUnsubscribe(UnsubscribeMessage msg) { { std::lock_guard g(m_mu); m_eventCallbacks.erase({msg.object, msg.eventName}); } writeFrame(encodeFrame(*m_codec, AnyMessage{std::move(msg)})); } template void RpcConnection::sendEvent(EventMessage msg) { writeFrame(encodeFrame(*m_codec, AnyMessage{std::move(msg)})); } template void RpcConnection::sendToken(TokenMessage msg) { writeFrame(encodeFrame(*m_codec, AnyMessage{std::move(msg)})); } template void RpcConnection::writeFrame(std::vector frame) { if (m_stopped.load()) return; auto self = this->shared_from_this(); boost::asio::post(m_strand, [self, frame = std::move(frame)]() mutable { self->m_writeQueue.push_back(std::move(frame)); if (!self->m_writing) { self->m_writing = true; self->doWrite(); } }); } template void RpcConnection::doWrite() { auto self = this->shared_from_this(); boost::asio::async_write(m_stream, boost::asio::buffer(m_writeQueue.front()), boost::asio::bind_executor(m_strand, [self](const boost::system::error_code& ec, std::size_t /*n*/) { if (ec) { self->fail(ec.message()); return; } self->m_writeQueue.pop_front(); if (self->m_writeQueue.empty()) { self->m_writing = false; } else { self->doWrite(); } })); } template void RpcConnection::fail(const std::string& reason) { bool expected = false; if (!m_stopped.compare_exchange_strong(expected, true)) return; // Fail every pending promise with a transport-level error. std::map>> calls; std::map>> methods; ErrorHandler errCb; { std::lock_guard g(m_mu); calls.swap(m_pendingCalls); methods.swap(m_pendingMethods); errCb.swap(m_error); m_eventCallbacks.clear(); } for (auto& [id, p] : calls) { ResultMessage r; r.id = id; r.ok = false; r.err = reason; r.errCode = "TRANSPORT_ERROR"; try { p->set_value(std::move(r)); } catch (...) {} } for (auto& [id, p] : methods) { MethodsResultMessage r; r.id = id; r.ok = false; r.err = reason; try { p->set_value(std::move(r)); } catch (...) {} } boost::system::error_code ignore; try { // lowest_layer() works for plain asio::ip::tcp::socket (returns // itself) and for asio::ssl::stream (returns the underlying TCP // socket). Closing the lowest layer tears the stack down cleanly // without needing protocol-specific shutdown sequences. m_stream.lowest_layer().close(ignore); } catch (...) {} // Notify the dispatch handler so it can drop any subscriptions still // keyed to this connection. Without this, a connection that drops // without sending Unsubscribe leaks sinks in the host's per-event map. if (m_handler) { try { m_handler->onConnectionClosed(static_cast(this)); } catch (...) {} } if (errCb) errCb(reason); } } // namespace logos::plain #endif // LOGOS_PLAIN_RPC_CONNECTION_H