POC: structured api interface #3946
Conversation
|
C++ single header API shape: // Generated by nim-brokers CBOR FFI codegen — do not edit.
//
// Header-only C++ wrapper around the C ABI declared in `logosdelivery.h`.
// Requires C++20 and jsoncons + jsoncons_ext/cbor in the include path.
#ifndef LOGOSDELIVERY_HPP
#define LOGOSDELIVERY_HPP
#include "logosdelivery.h"
#include <jsoncons/json.hpp>
#include <jsoncons_ext/cbor/cbor.hpp>
#include <cstdint>
#include <cstring>
#include <functional>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <system_error>
#include <unordered_map>
#include <utility>
#include <vector>
namespace logosdelivery {
template <typename T>
class Result {
std::optional<T> value_;
std::string error_;
public:
static Result<T> ok(T value) {
Result<T> r;
r.value_ = std::move(value);
return r;
}
static Result<T> err(std::string message) {
Result<T> r;
r.error_ = std::move(message);
return r;
}
bool isOk() const { return value_.has_value(); }
bool isErr() const { return !value_.has_value(); }
explicit operator bool() const { return isOk(); }
const T& value() const { return *value_; }
T& value() { return *value_; }
const T& operator*() const { return *value_; }
const T* operator->() const { return &*value_; }
T&& take() { return std::move(*value_); }
const std::string& error() const { return error_; }
};
template <>
class Result<void> {
bool ok_ = true;
std::string error_;
public:
static Result<void> ok() {
Result<void> r;
r.ok_ = true;
return r;
}
static Result<void> err(std::string message) {
Result<void> r;
r.ok_ = false;
r.error_ = std::move(message);
return r;
}
Result() = default;
bool isOk() const { return ok_; }
bool isErr() const { return !ok_; }
explicit operator bool() const { return isOk(); }
const std::string& error() const { return error_; }
};
struct Bytes : std::vector<uint8_t> {
using std::vector<uint8_t>::vector;
Bytes() = default;
explicit Bytes(std::vector<uint8_t> v) : std::vector<uint8_t>(std::move(v)) {}
};
// ---- Enums ----
enum class PagingDirection : int32_t {
BACKWARD = 0,
FORWARD = 1,
};
enum class ConnectionStatus : int32_t {
Disconnected = 0,
PartiallyConnected = 1,
Connected = 2,
};
enum class WakuMode : int32_t {
Core = 0,
Edge = 1,
};
enum class NodeInfoId : int32_t {
Version = 0,
Metrics = 1,
MyMultiaddresses = 2,
MyENR = 3,
MyPeerId = 4,
MyBoundPorts = 5,
MyMixPubKey = 6,
};
// ---- Object payload structs ----
struct MessageSentEvent;
struct MessageErrorEvent;
struct MessagePropagatedEvent;
struct WakuMessage;
struct MessageReceivedEvent;
struct Subscribe;
struct Unsubscribe;
struct MessageEnvelope;
struct ChannelMessageReceivedEvent;
struct ChannelMessageSentEvent;
struct ChannelMessageErrorEvent;
struct CloseChannel;
struct ReceivedMessage;
struct StoreQueryResponse;
struct WakuMessageKeyValue;
struct StoreQueryRequest;
struct ConnectionStatusChangeEvent;
struct StartAsNode;
struct Shutdown;
struct Stop;
// ---- Distinct / alias types ----
using RequestId = std::string;
using ContentTopic = std::string;
using Timestamp = int64_t;
using ChannelId = std::string;
using SdsParticipantID = std::string;
using PubsubTopic = std::string;
using RelayConnectedPeers = std::vector<std::string>;
using RelayPeersInMesh = std::vector<std::string>;
using WakuMessageHash = std::vector<uint8_t>;
using PeerIdsFromPeerstore = std::vector<std::string>;
using ConnectedPeersInfo = std::vector<std::string>;
using ConnectedPeers = std::vector<std::string>;
using PeerIdsByProtocol = std::vector<std::string>;
using DnsDiscovery = std::vector<std::string>;
using ListenAddresses = std::vector<std::string>;
struct MessageSentEvent {
RequestId requestId{};
std::string messageHash{};
};
struct MessageErrorEvent {
RequestId requestId{};
std::string messageHash{};
std::string error{};
};
struct MessagePropagatedEvent {
RequestId requestId{};
std::string messageHash{};
};
struct WakuMessage {
Bytes payload{};
ContentTopic contentTopic{};
Bytes meta{};
uint32_t version{};
Timestamp timestamp{};
bool ephemeral{};
Bytes proof{};
};
struct MessageReceivedEvent {
std::string messageHash{};
WakuMessage message{};
};
struct Subscribe {
};
struct Unsubscribe {
};
struct MessageEnvelope {
ContentTopic contentTopic{};
Bytes payload{};
bool ephemeral{};
Bytes meta{};
};
struct ChannelMessageReceivedEvent {
ChannelId channelId{};
SdsParticipantID senderId{};
Bytes payload{};
};
struct ChannelMessageSentEvent {
ChannelId channelId{};
RequestId requestId{};
};
struct ChannelMessageErrorEvent {
ChannelId channelId{};
RequestId requestId{};
std::string error{};
};
struct CloseChannel {
};
struct ReceivedMessage {
PubsubTopic pubsubTopic{};
WakuMessage message{};
};
struct StoreQueryResponse {
std::string requestId{};
uint32_t statusCode{};
std::string statusDesc{};
std::vector<WakuMessageKeyValue> messages{};
std::optional<WakuMessageHash> paginationCursor{};
};
struct WakuMessageKeyValue {
WakuMessageHash messageHash{};
std::optional<WakuMessage> message{};
std::optional<PubsubTopic> pubsubTopic{};
};
struct StoreQueryRequest {
std::string requestId{};
bool includeData{};
std::optional<PubsubTopic> pubsubTopic{};
std::vector<ContentTopic> contentTopics{};
std::optional<Timestamp> startTime{};
std::optional<Timestamp> endTime{};
std::vector<WakuMessageHash> messageHashes{};
std::optional<WakuMessageHash> paginationCursor{};
PagingDirection paginationForward{};
std::optional<uint64_t> paginationLimit{};
};
struct ConnectionStatusChangeEvent {
ConnectionStatus connectionStatus{};
};
class MessagingClientInterface;
class ReliableChannelManagerInterface;
class KernelInterface;
struct __InstanceCtxEnvelope {
std::optional<uint64_t> ok;
std::optional<std::string> err;
};
namespace detail {
template <typename Owner, typename Traits>
class EventDispatcher;
struct MessageSentEventEventTraits;
struct MessageErrorEventEventTraits;
struct MessagePropagatedEventEventTraits;
struct MessageReceivedEventEventTraits;
struct ChannelMessageReceivedEventEventTraits;
struct ChannelMessageSentEventEventTraits;
struct ChannelMessageErrorEventEventTraits;
struct ReceivedMessageEventTraits;
struct ConnectionStatusChangeEventEventTraits;
} // namespace detail
class Logosdelivery {
public:
Logosdelivery();
~Logosdelivery();
Logosdelivery(const Logosdelivery&) = delete;
Logosdelivery& operator=(const Logosdelivery&) = delete;
Logosdelivery(Logosdelivery&&) = delete;
Logosdelivery& operator=(Logosdelivery&&) = delete;
static std::string_view version() noexcept;
Result<void> createContext();
bool validContext() const noexcept;
explicit operator bool() const noexcept;
void shutdown() noexcept;
uint32_t ctx() const noexcept;
Result<void> startAsNode(std::string config);
Result<std::unique_ptr<MessagingClientInterface>> startAsClient(WakuMode mode, std::string preset);
Result<void> shutdown();
Result<void> stop();
Result<std::unique_ptr<KernelInterface>> kernel();
Result<std::unique_ptr<MessagingClientInterface>> messaging();
Result<std::unique_ptr<ReliableChannelManagerInterface>> channels();
Result<std::string> getNodeInfo(NodeInfoId id);
Result<std::string> getAvailableConfigs();
using ConnectionStatusChangeEventCallback = std::function<void(Logosdelivery&, ConnectionStatus connectionStatus)>;
uint64_t onConnectionStatusChangeEvent(ConnectionStatusChangeEventCallback fn) noexcept;
void offConnectionStatusChangeEvent(uint64_t handle = 0) noexcept;
std::string listApis();
std::string getSchema();
private:
uint32_t ctx_ = 0;
using ConnectionStatusChangeEventDispatcher = detail::EventDispatcher<Logosdelivery, detail::ConnectionStatusChangeEventEventTraits>;
std::unique_ptr<ConnectionStatusChangeEventDispatcher> connection_status_change_eventDispatcher_;
};
// ---- MessagingClientInterface — sub-instance wrapper of MessagingClientInterface ----
class MessagingClientInterface {
private:
uint32_t ctx_ = 0;
using MessageSentEventDispatcher = detail::EventDispatcher<MessagingClientInterface, detail::MessageSentEventEventTraits>;
std::unique_ptr<MessageSentEventDispatcher> message_sent_eventDispatcher_;
using MessageErrorEventDispatcher = detail::EventDispatcher<MessagingClientInterface, detail::MessageErrorEventEventTraits>;
std::unique_ptr<MessageErrorEventDispatcher> message_error_eventDispatcher_;
using MessagePropagatedEventDispatcher = detail::EventDispatcher<MessagingClientInterface, detail::MessagePropagatedEventEventTraits>;
std::unique_ptr<MessagePropagatedEventDispatcher> message_propagated_eventDispatcher_;
using MessageReceivedEventDispatcher = detail::EventDispatcher<MessagingClientInterface, detail::MessageReceivedEventEventTraits>;
std::unique_ptr<MessageReceivedEventDispatcher> message_received_eventDispatcher_;
public:
explicit MessagingClientInterface(uint32_t ctx) noexcept;
~MessagingClientInterface();
MessagingClientInterface(const MessagingClientInterface&) = delete;
MessagingClientInterface& operator=(const MessagingClientInterface&) = delete;
MessagingClientInterface(MessagingClientInterface&&) = delete;
MessagingClientInterface& operator=(MessagingClientInterface&&) = delete;
uint32_t ctx() const noexcept { return ctx_; }
bool valid() const noexcept { return ctx_ != 0; }
explicit operator bool() const noexcept { return ctx_ != 0; }
void close() noexcept;
Result<void> subscribe(ContentTopic contentTopic);
Result<void> unsubscribe(ContentTopic contentTopic);
Result<RequestId> send(MessageEnvelope envelope);
using MessageSentEventCallback = std::function<void(MessagingClientInterface&, RequestId requestId, std::string_view messageHash)>;
uint64_t onMessageSentEvent(MessageSentEventCallback fn) noexcept;
void offMessageSentEvent(uint64_t handle = 0) noexcept;
using MessageErrorEventCallback = std::function<void(MessagingClientInterface&, RequestId requestId, std::string_view messageHash, std::string_view error)>;
uint64_t onMessageErrorEvent(MessageErrorEventCallback fn) noexcept;
void offMessageErrorEvent(uint64_t handle = 0) noexcept;
using MessagePropagatedEventCallback = std::function<void(MessagingClientInterface&, RequestId requestId, std::string_view messageHash)>;
uint64_t onMessagePropagatedEvent(MessagePropagatedEventCallback fn) noexcept;
void offMessagePropagatedEvent(uint64_t handle = 0) noexcept;
using MessageReceivedEventCallback = std::function<void(MessagingClientInterface&, std::string_view messageHash, const WakuMessage& message)>;
uint64_t onMessageReceivedEvent(MessageReceivedEventCallback fn) noexcept;
void offMessageReceivedEvent(uint64_t handle = 0) noexcept;
};
// ---- ReliableChannelManagerInterface — sub-instance wrapper of ReliableChannelManagerInterface ----
class ReliableChannelManagerInterface {
private:
uint32_t ctx_ = 0;
using ChannelMessageReceivedEventDispatcher = detail::EventDispatcher<ReliableChannelManagerInterface, detail::ChannelMessageReceivedEventEventTraits>;
std::unique_ptr<ChannelMessageReceivedEventDispatcher> channel_message_received_eventDispatcher_;
using ChannelMessageSentEventDispatcher = detail::EventDispatcher<ReliableChannelManagerInterface, detail::ChannelMessageSentEventEventTraits>;
std::unique_ptr<ChannelMessageSentEventDispatcher> channel_message_sent_eventDispatcher_;
using ChannelMessageErrorEventDispatcher = detail::EventDispatcher<ReliableChannelManagerInterface, detail::ChannelMessageErrorEventEventTraits>;
std::unique_ptr<ChannelMessageErrorEventDispatcher> channel_message_error_eventDispatcher_;
public:
explicit ReliableChannelManagerInterface(uint32_t ctx) noexcept;
~ReliableChannelManagerInterface();
ReliableChannelManagerInterface(const ReliableChannelManagerInterface&) = delete;
ReliableChannelManagerInterface& operator=(const ReliableChannelManagerInterface&) = delete;
ReliableChannelManagerInterface(ReliableChannelManagerInterface&&) = delete;
ReliableChannelManagerInterface& operator=(ReliableChannelManagerInterface&&) = delete;
uint32_t ctx() const noexcept { return ctx_; }
bool valid() const noexcept { return ctx_ != 0; }
explicit operator bool() const noexcept { return ctx_ != 0; }
void close() noexcept;
Result<ChannelId> createReliableChannel(ChannelId channelId, ContentTopic contentTopic, SdsParticipantID senderId);
Result<void> closeChannel(ChannelId channelId);
Result<RequestId> sendOnChannel(ChannelId channelId, Bytes payload, bool ephemeral);
using ChannelMessageReceivedEventCallback = std::function<void(ReliableChannelManagerInterface&, ChannelId channelId, SdsParticipantID senderId, std::span<const uint8_t> payload)>;
uint64_t onChannelMessageReceivedEvent(ChannelMessageReceivedEventCallback fn) noexcept;
void offChannelMessageReceivedEvent(uint64_t handle = 0) noexcept;
using ChannelMessageSentEventCallback = std::function<void(ReliableChannelManagerInterface&, ChannelId channelId, RequestId requestId)>;
uint64_t onChannelMessageSentEvent(ChannelMessageSentEventCallback fn) noexcept;
void offChannelMessageSentEvent(uint64_t handle = 0) noexcept;
using ChannelMessageErrorEventCallback = std::function<void(ReliableChannelManagerInterface&, ChannelId channelId, RequestId requestId, std::string_view error)>;
uint64_t onChannelMessageErrorEvent(ChannelMessageErrorEventCallback fn) noexcept;
void offChannelMessageErrorEvent(uint64_t handle = 0) noexcept;
};
// ---- KernelInterface — sub-instance wrapper of KernelInterface ----
class KernelInterface {
private:
uint32_t ctx_ = 0;
using ReceivedMessageDispatcher = detail::EventDispatcher<KernelInterface, detail::ReceivedMessageEventTraits>;
std::unique_ptr<ReceivedMessageDispatcher> received_messageDispatcher_;
public:
explicit KernelInterface(uint32_t ctx) noexcept;
~KernelInterface();
KernelInterface(const KernelInterface&) = delete;
KernelInterface& operator=(const KernelInterface&) = delete;
KernelInterface(KernelInterface&&) = delete;
KernelInterface& operator=(KernelInterface&&) = delete;
uint32_t ctx() const noexcept { return ctx_; }
bool valid() const noexcept { return ctx_ != 0; }
explicit operator bool() const noexcept { return ctx_ != 0; }
void close() noexcept;
Result<ContentTopic> buildContentTopic(std::string appName, uint32_t appVersion, std::string name, std::string encoding);
Result<PubsubTopic> buildPubsubTopic(std::string topicName);
Result<PubsubTopic> defaultPubsubTopic();
Result<int64_t> relayPublish(PubsubTopic pubsubTopic, WakuMessage message, uint32_t timeoutMs);
Result<bool> relaySubscribe(PubsubTopic pubsubTopic);
Result<bool> relayUnsubscribe(PubsubTopic pubsubTopic);
Result<bool> relayAddProtectedShard(uint16_t clusterId, uint16_t shardId, std::string publicKey);
Result<RelayConnectedPeers> relayConnectedPeers(PubsubTopic pubsubTopic);
Result<RelayPeersInMesh> relayPeersInMesh(PubsubTopic pubsubTopic);
Result<bool> filterSubscribe(std::optional<PubsubTopic> pubsubTopic, std::vector<ContentTopic> contentTopics, std::string peer);
Result<bool> filterUnsubscribe(std::optional<PubsubTopic> pubsubTopic, std::vector<ContentTopic> contentTopics, std::string peer);
Result<bool> filterUnsubscribeAll(std::string peer);
Result<std::string> lightpushPublish(PubsubTopic pubsubTopic, WakuMessage message, std::string peer);
Result<StoreQueryResponse> storeQuery(StoreQueryRequest request, std::string peer, int64_t timeoutMs);
Result<bool> connect(std::vector<std::string> peers, uint32_t timeoutMs);
Result<bool> disconnectPeerById(std::string peerId);
Result<bool> disconnectAllPeers();
Result<bool> dialPeer(std::string peerAddr, std::string protocol, int64_t timeoutMs);
Result<bool> dialPeerById(std::string peerId, std::string protocol, int64_t timeoutMs);
Result<PeerIdsFromPeerstore> peerIdsFromPeerstore();
Result<ConnectedPeersInfo> connectedPeersInfo();
Result<ConnectedPeers> connectedPeers();
Result<PeerIdsByProtocol> peerIdsByProtocol(std::string protocol);
Result<DnsDiscovery> dnsDiscovery(std::string enrTreeUrl, std::string nameServer, int64_t timeoutMs);
Result<bool> discv5UpdateBootnodes(std::vector<std::string> bootnodes);
Result<bool> startDiscv5();
Result<bool> stopDiscv5();
Result<int64_t> peerExchangeRequest(uint64_t numPeers);
Result<std::string> version();
Result<ListenAddresses> listenAddresses();
Result<std::string> myEnr();
Result<std::string> myPeerId();
Result<std::string> metrics();
Result<bool> isOnline();
Result<int64_t> pingPeer(std::string peerAddr, int64_t timeoutMs);
using ReceivedMessageCallback = std::function<void(KernelInterface&, PubsubTopic pubsubTopic, const WakuMessage& message)>;
uint64_t onReceivedMessage(ReceivedMessageCallback fn) noexcept;
void offReceivedMessage(uint64_t handle = 0) noexcept;
}; |
| # WakuMessage was elevated to logos_delivery/api/types; re-exported here so | ||
| # existing call sites are unaffected. | ||
| from logos_delivery/api/types import WakuMessage | ||
| export WakuMessage |
There was a problem hiding this comment.
I prefer having that WakuMessage to remain at this level.
There was a problem hiding this comment.
Each layer will have to publish all types that it needs to speak with any upper layers. WakuMessage is a layer API type ("message" types usually are as they end up as arguments to the API procs).
In the C/FFI domain, we have complete control of what types actually make it out to users so we can hide WakuMessage if we want to.
We could try to have some kind of distinction in the Nim side e.g. differentiate "Layer interface" (procs, types) vs. "Nim library interface." We can achieve that by having a global nim "api" folder somewhere that imports selectively from the individual Nim layer API folders -- that ends up being the official API for "Nim Logos Delivery."
| @@ -0,0 +1,83 @@ | |||
| --- | |||
| name: gitnexus-cli | |||
| description: "Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: \"Index this repo\", \"Reanalyze the codebase\", \"Generate a wiki\"" | |||
There was a problem hiding this comment.
Let's add all these nexus files in a separate PR.
|
|
||
| export types | ||
|
|
||
| BrokerInterface(API, KernelInterface): |
There was a problem hiding this comment.
I see that API tries to flag this macro as FFI boundary.
I thought we mentioned that we would leave the ffi work aside for now and only focus on internal layering.
There was a problem hiding this comment.
An idea would be to at least pilot Broker FFI for the Kernel C/FFI surface, since that is the one we own completely and is, at least de jure, unsupported.
Another idea is to just allow this in, since we are not forced to use it as a back-end for the library. It documents what the FFI upgrade would look like from both Nim and C side. We can then run experiments where we wire the C/FFI surface to either ffi provider and run tests.
For this PR in particular: it is impossible to size-constrain a refactor of this type and scope. I tried; it's impossible. This is not a routine maintenance or your everyday feature; this is a massive architectural jump that is already stripped to its barest form. If we land this PR (and we should, absolutely) we won't see another one like this for several years. |
fcecin
left a comment
There was a problem hiding this comment.
This PR is fundamental and we have to prioritize it and merge it ASAP. It just needs (a) dedicated tests (start/stop tests, config tests, ...), and (b) any downstream fixes from test writing.
We can sort the config situation as we move forward. For now, it is fine to keep the single WakuNodeConf, the hardcoded channels prototype confs per channel, the p2pReliability flag for messaging api, etc.
| return err("cannot mount messaging client on a started node") | ||
|
|
||
| try: | ||
| var conf: WakuNodeConf |
There was a problem hiding this comment.
This bare var conf: WakuNodeConf I think needs, unfortunately, to be (something like):
var conf = defaultWakuNodeConf().valueOr:
return err("Failed creating default conf: " & error)
For the same reasion tools/confutils/conf_from_json.nim currently has to do it (var conf = ?defaultWakuNodeConf()) -- that's in master (with my PR that extracted the config JSON parser). But in this PR, you can find the same behavior in liblogosdelivery/logos_delivery_api/node_api.nim:
registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]):
proc(configJson: cstring): Future[Result[string, string]] {.async.} =
## Parse the JSON configuration using fieldPairs approach (WakuNodeConf)
var conf = defaultWakuNodeConf().valueOr:
return err("Failed creating default conf: " & error)
Apps do the same right now: examples/api_example/api_example.nim does var conf = defaultWakuNodeConf() then sets mode/preset.
We will for sure continue to refactor the config code (to e.g. truly centralize config defaults somewhere in definitive that's globally accessible). I won't submit any more config refactor PRs before this PR lands, since the API shape is what's going to constrain and to point to what we have to do, config-wise. For now we have to go through the whole pipeline as it is, mixing layers of "cli"-coded stuff, builder, etc., to make sure we concentrate and honor the scattered defaults as much as possible.
There was a problem hiding this comment.
Thanks it's a good catch. Yes this is just mimic of current state of config and open for change.
| return err( | ||
| "already started as node; cannot start as client, but you can use as client" | ||
| ) | ||
| if self.waku.node.started: |
There was a problem hiding this comment.
The line above this is demanding self.waku to be nil, so this self.waku.node.started is a guaranteed nil deref on <nil>.node.
There was a problem hiding this comment.
You're right, the latter condition will be removed.
Ivansete-status
left a comment
There was a problem hiding this comment.
Some more comments/questions. Thanks!
|
|
||
| export ikernel_iface, imessagingclient_iface, ireliablechannelmanager_iface | ||
|
|
||
| BrokerInterface(API, LogosDeliveryInterface): |
There was a problem hiding this comment.
This file is also only aiming for ffi purposes, and is not invoked internally by other Nim module. This can go in a separate discussion/PR. Besides, we already have nim-ffi for that purpose :)
There was a problem hiding this comment.
Yepp, let's discuss this separately. As said FFI exercise here is also a validation point as it really compiles to some meaningful. It's only API that enables FFI together with registerBrokerLibrary macro and compile time flag -d:BrokerFfiApi, that's it. Without it works as native Nim nim-brokers.
| export reliable_channel | ||
|
|
||
| type ReliableChannelManager* = ref object | ||
| type ReliableChannelManager* = ref object of ReliableChannelManagerInterface |
There was a problem hiding this comment.
I might be missing something but this polymophism cost seems not needed.
I believe the intent is to make it mockable for testing but the way how the base interface is declared, logos_delivery/api/reliable_channel_manager_interface.nim, seems not idiomatic, i.e., I'd expect seeing either:
- base-annotated procs
- "concept" technique ( even though we never used that .)
- Use of generics
There was a problem hiding this comment.
Use of ref object of... is to achieve familiar shape for everyone also enables real inheritance features of nim.
Yes concept can be useful and interesting exercise I agree, here what we gain with the broker concept is real runtime mock-able interface out of the box.
=> https://github.com/NagyZoltanPeter/nim-brokers/blob/master/doc/OOP_Brokers.md#testing--mocking-providers
|
|
||
| client = (await createNode(conf)).valueOr: | ||
| raiseAssert error | ||
| client.mountMessagingClient().isOkOr: |
There was a problem hiding this comment.
I saw mountMessagingClient proc got deleted. Then I checked that make test tests/api/test_api_health.nim seems to not compile.
There was a problem hiding this comment.
Yes, tests are not yet adapted. WIP
| mc: MessagingClient | ||
| rcm: ReliableChannelManager |
There was a problem hiding this comment.
| mc: MessagingClient | |
| rcm: ReliableChannelManager | |
| messagingClient: MessagingClient | |
| reliableChannelManager: ReliableChannelManager |
It is better to be explicit on attribute names, but this is a tiny detail. Something more relevant is that messagingClient is lazily created whereas reliableChannelManager is eagerly created. Why this difference?
I'm not saying is wrong, I just want to learn why the lazy init is needed in those cases.
There was a problem hiding this comment.
Ok, I agree.
Initialization difference is coming from the use case difference.
We all the time need kernel/waku instance regardless how we start.
MessagingClient is needed if we start as a client - startAsClient - that is eagerly init messagingClient ahead of call messaging interface accessor. While we can't say the user will need reliableChannelManager until the user explicitly ask for it.
|
|
||
| return err("kernel not yet implemented") | ||
|
|
||
| method messaging( |
There was a problem hiding this comment.
This part is very interesting. Thanks :)
It inspired this: logos-messaging/nim-ffi#80 because this is a real need to allow using class-handle OOP style types instead of just flat usage.
There was a problem hiding this comment.
Happy to inspire nim-ffi... but raises the question why mimic features exists and usable right now.
Right now I don't see advance of nim-ffi over using nim-brokers.
There was a problem hiding this comment.
I respect your opinion and I will always do.
For now, I believe it is better to focus our energies on having a proper Nim-only layering and keep using nim-ffi.
nim-ffi is the current FFI solution that we have. Let's use it for now. And if something is not good, file an issue there and we'll prioritize asap.
|
|
||
| return ok(ReliableChannelManagerInterface(self.rcm)) | ||
|
|
||
| method startAsNode( |
There was a problem hiding this comment.
These two startAs* look very interesting too, even though they seem private and not used so far but I like the idea to differentiate by use case. ( I know they are exposed through FFI but I like more the explicit .ffi. pragma to annotate a proc that is crossing FFI boundaries.)
There was a problem hiding this comment.
Ok, there is a broker machinery behind these "overrides" - they are not real overrides because every call through either the interface reference or the implementation reference is go through broker delivery.
So when you call startAsNode from nim it translates to StartAsNode.request(...).
It has nothing to do with FFI, but enables that too.
The brokers comes with this exact concept, you don't code for FFI, you code in nim without care from where it will be used. That's how it makes using it natural and similar to use as nim library interface or using from other languages (That's what it aims if you look at the generated FFI interfaces - they look exactly how you defined them in Nim).
So to say look at the startAs... interfaces not as FFI interfaces but library interfaces.
Probably the one good exercise can be to rewrite wakunode2 with the use of the new API shape.
There was a problem hiding this comment.
Okay,
Why perform that broker machinery request, StartAsNode.request(...), if all the components are directly accessible to the LogosDelivery object?
There was a problem hiding this comment.
That's hidden in this case.
From dev point you issue logosDelivery.startAsNode().
What's inside is part of interfacing. Particularly it the OOP styled broker interface is just a convenient look and feel - a facade.
You could simply do declare your interface as naked Request/EventBrokers - as per the original meaning so you don't need to know who is LogosDelivery class and instance. That would be a pure naked - and optimal - decoupled interface.
With the facade you can write:
let logosDelivery = LogosDeliveryInterface.create()
discard (await logosDelivery.startAsNode(...)).valueOr:
error ...or
let logosDelivery = LogosDeliveryInterface.create()
(await logosDelivery.startAsClient(Core, "logos.dev")).isOkOr:
....
let rcm = logosDelivery.channels().valueOr:
error ...
let channelId = (await rcm.createReliableChannel(...)).valueOr:
error ....
let requestId = (await rmc.sendOnChannel(channelId, @[1,2,3], false)).valueOr:
error ....Based on the interface only.
| proc setupProviders(ctx: BrokerContext): Result[void, string] = | ||
| ## Called by registerBrokerLibrary on the processing thread: construct the | ||
| ## main facade impl adopting the FFI context, wiring its providers under `ctx`. | ||
| discard LogosDelivery.createUnderContext(ctx) | ||
| ok() | ||
|
|
||
| # DI factory registration: non ffi - nim lib users needs it. | ||
| LogosDeliveryInterface.provideFactory( | ||
| proc(): Result[LogosDeliveryInterface, string] {.gcsafe.} = | ||
| ok(LogosDeliveryInterface(LogosDelivery.createUnderContext(NewBrokerContext()))) |
There was a problem hiding this comment.
Interesting!, may elaborate the purpose of these two?
There was a problem hiding this comment.
setupProviders (yes I was wrong) belongs to FFI machinery - that is a mandatory interface that is being called right from the processing thread at startup. My concept need a single point where at least initializer and shut down request brokers are instantiated - implemented. Other than that every other broker can be lazily provided
It is perfectly doable that kernel api is not available just in special case... not provided request broker is just an error return, nothing breaks.
There was a problem hiding this comment.
providerFactory is a real factory interface hook where you are able to implement conditional implementation instantiation.
There was a problem hiding this comment.
Okay, by "conditional implementation" you mean for example the lazy init of ReliableChannelManager?
There was a problem hiding this comment.
No, it's the implementation point for dependency injection - useful when needed.
So you decide what to instantiate, based upon you config / condition.
It's being used when some says:
let instance = LogosDeliveryInterface.create()it allows you to define arguments also of course.
|
Thanks for the recent simplification. Nevertheless, I see the PR is too complex and is adding deep architectural changes. I envision that the next movement we need to have in master is to create a plain Nim The |
Let me disagree a bit here. Due I feel no gain with only doing that.
We are on the same page :-) |
|
@Ivansete-status @NagyZoltanPeter - TLDR: We need this PR merged to be able finish every other API / config issue that we have downstream from these improvements, so FYI I'm counting on this being merged somewhat soon. Otherwise I'll have to start working on something that will end up being a duplicate of this, essentially, so we can do the API shape refactor (overrides), the new config objects, the new kernel API, and Store API access. |
Currently all tests build and run, libraries and their examples, wakunode2 builds and can run. |
Squash of the initial structured-API POC work (1d684bf..c37dcf8): - WIP: structured Broker API interface + impl (Logos Delivery facade) - Restructured folders; interfaces under logos_delivery/api; MessagingClient and ReliableChannelManager reworked as BrokerInterface/BrokerImplement - API types elevated under logos_delivery/api/types.nim - onelogosdelivery target; wakunode2 / FFI lib build fixes - nim-brokers integration + version bumps; git_version into FFI lib version - compile fixes for tests
535fe2c to
47f3194
Compare
|
You can find the image built from this PR at Built from 8526a7b |
| let decRes = await Decrypt.request(payload) | ||
| let plaintext = decRes.valueOr: | ||
| MessageErrorEvent.emit( | ||
| ### TODO: Emitting of events from another layer is not completly ok to do so. |
There was a problem hiding this comment.
A layer is what you'd draw as a solid "product" box in a diagram: it's something you can remove and replace with something else. You'd be able to come up with the concept of reliable channels later and implement them on top of the messaging product. We could implement two or three channels-like features on top of the messaging layer, each with different features and implementation strategies, and people would be able to choose to mount any one of them.
Channels isn't really like that. It's more of a subsystem of the messaging layer. If you conceive of the reliable channels feature as an actually-independent product layer then it would be done slightly differently.
Now, it's fine (and good) to keep channels where it is right now in the source tree, but we can treat it as a component (that's what the directories are) that together with the messaging component, implements the Messaging/Client layer. So we have two layers only instead of three, and we avoid forcing a level of decoupling between subsystems of what is essentially the same layer.
If we go with that interpretation, then this is no longer a layering violation (by definition) and is in fact entirely fine. So what we'd do is not attribute the qualifier "layer" to a thing that just happens to sit into some height in the source tree. Just because it has a neatly separate directory doesn't mean it is a software layer.
So we would be productizing "Kernel" and "Messaging," and that's going to be the only two layers in the foreseeable future. If we want that interpretation, which also rhymes with "startAsClient" and "startAsNode" -- one start per actual product, per actual layer.
We could also switch this to emit a ChannelErrorEvent later, but that's beside this point here -- the point is that that's an intra-layer decision then.
There was a problem hiding this comment.
What I really meant, is this sounds more like a feature request for messaging client - but it might just result creating noise. Here the MessageErrorEvent is used for internal communication (which can be valid at a point) and will turn to Channel related error event.
The only problem user maybe confused he/she can subscribe to both layer messages.
There was a problem hiding this comment.
Yep, sure. I'm just latching onto the "layering violation" claim there before this idea spreads. This is my fault; I came up with the "three layers" idea and I have to collect that trash before it spreads further. We have two layers, not three. Channels is a subsystem, not a layer, so we may have ergonomy problems or API exposure problems (as you describe there) but not "accessing a thing from another layer." That's all I'm saying.
|
I have added my PR review and suggested fixes as a separate PR that can be merged into this PR branch: #3966 There's no need to merge it. It's just there to demonstrate the fixes. It can be cherry-picked, reinterpreted with other code changes here, or just closed. EDIT: @NagyZoltanPeter |
Ivansete-status
left a comment
There was a problem hiding this comment.
Thanks for the recent changes!
I'm adding some more comments after a third round of review but in this occasion, deeper.
I will try to complete the review this evening or next week.
Cheers!
|
|
||
| var jsonNode = newJObject() | ||
| jsonNode["configOptions"] = configOptionDetails | ||
| let asString = pretty(jsonNode) |
There was a problem hiding this comment.
Dead variable
| let asString = pretty(jsonNode) |
| method getNodeInfo( | ||
| self: LogosDelivery, id: NodeInfoId | ||
| ): Future[Result[string, string]] {.async.} = |
There was a problem hiding this comment.
nit, this should not be async. The use of not async code within async proc may impact the chronos loop performance.
Applies elsewhere.
| method getNodeInfo( | |
| self: LogosDelivery, id: NodeInfoId | |
| ): Future[Result[string, string]] {.async.} = | |
| method getNodeInfo( | |
| self: LogosDelivery, id: NodeInfoId | |
| ): Result[string, string] = |
There was a problem hiding this comment.
Ok, it can be done.
| proc new*(T: typedesc[MessagingClient], useP2PReliability: bool, node: WakuNode): T = | ||
| let sendService = SendService.new(useP2PReliability, node).valueOr: | ||
| error "Failed to initialize SendService", error = error | ||
| quit(QuitFailure) |
There was a problem hiding this comment.
We should not invoke quit in so low level. This should be only in app code, not in API defs. Let's make it return the error up to the caller.
There was a problem hiding this comment.
Yes, it's a leftover because meanwhile nim-brokers are fixed to manage new ctors with Result[T, string] and Future[Result[T, string]] signatures as well. So we can move back to error managing new ctor.
| method subscribe( | ||
| self: MessagingClient, contentTopic: ContentTopic | ||
| ): Future[Result[void, string]] {.async.} = | ||
| return self.node.subscriptionManager.subscribe(contentTopic) | ||
|
|
||
| method unsubscribe( | ||
| self: MessagingClient, contentTopic: ContentTopic | ||
| ): Future[Result[void, string]] {.async.} = | ||
| return self.node.subscriptionManager.unsubscribe(contentTopic) |
There was a problem hiding this comment.
These two methods are never invoked.
| method subscribe( | |
| self: MessagingClient, contentTopic: ContentTopic | |
| ): Future[Result[void, string]] {.async.} = | |
| return self.node.subscriptionManager.subscribe(contentTopic) | |
| method unsubscribe( | |
| self: MessagingClient, contentTopic: ContentTopic | |
| ): Future[Result[void, string]] {.async.} = | |
| return self.node.subscriptionManager.unsubscribe(contentTopic) |
There was a problem hiding this comment.
Not yet, it is to replace api procs under logos_delivery/waku/api.
My view those entry points place is at MessagingClient not under Waku.
So current is an intermediate state where both lives together.
| ): Future[Result[void, string]] {.async.} = | ||
| return self.node.subscriptionManager.unsubscribe(contentTopic) | ||
|
|
||
| method send( |
There was a problem hiding this comment.
This should be annotated as public so that it is easier for future lookups.
Also, I see future confusion when someone else comes and comes across this method that is marked as private but it can be used somewhere else.
| method send( | |
| method send*( |
There was a problem hiding this comment.
Great catch, I need to cross check this in nim-brokers if it supported and if not, need a fix.
| return ok() | ||
|
|
||
| w.messagingClient = | ||
| MessagingClient.createUnderContext(w.brokerCtx, w.conf.p2pReliability, w.node) |
There was a problem hiding this comment.
What is the purpose of createUnderContext proc? It seems it does some great unknown magic. I'd like to have something more explicit and clearer, though.
In any case, this is not an idiomatic manner of creating a ref object. It should be MessagingClient.new(...).
This also applies to ReliableChannelManager below.
There was a problem hiding this comment.
Yes, the create and creatUnderContext is BrokerImplementation machinery.
That uses and embeds the ref object's new and takes care about broker context setup for the whole implementation wiring.
So the implementation classes can be instantiated with the new ctor of course still, but create or createUnderContext is used to make fulfill the ancestor interface.
| except CatchableError as e: | ||
| return err(e.msg) | ||
|
|
||
| method relayUnsubscribe( |
There was a problem hiding this comment.
All these ~35 added methods are never invoked by any other Nim module.
They are trying to collapse something that is already properly separated, well defined, and controlled, within library/kernel_api/protocols/* and these new suggested methods are there to eventually replace them.
Therefore, all these methods are out of scope of the needed Nim API refactor.
There was a problem hiding this comment.
Interesting, but I think the mean of layering APIs and unify access to them is a goal. So in other words any nim module would use our library, should also access protocol layer through KernelInterface and not via deeper level, knowing protocols, peer manager, etc.
I think even the ffi layer should not live outside logos_delivery folder in the future. WDYT?
There was a problem hiding this comment.
I think we should have one clear entry point, i.e., LogosDelivery, that composes the three layers. Then this entry point can be eventually be consumed by any other conumer: lib; REST; or another Nim app.
There was a problem hiding this comment.
Yes, than the only question is the abstract interface here, no? Becuase this POC suggest, that any can useLogosDelivery as nim library through it's interface (as factory provider will instantiate the right implementation).
|
@NagyZoltanPeter PR description formatting seem to be broken, like code snippets are "inverted". |
Thanks, it good now. |
There was a problem hiding this comment.
Shouldn't this be in CLAUDE.md of nim-brokers itself?
Or if it must retain here, then can we put it in ./docs?
|
|
||
| export types | ||
|
|
||
| BrokerInterface(KernelInterface): |
There was a problem hiding this comment.
Can you please explain what's the benefit of using brokers here?
I'm not a Nim expert, but it seems to me that the same behaviour could be achieved with plain Nim, using concept and generics. Wouldn't it be simpler?
There was a problem hiding this comment.
My main concern regarding this PR is how many things are presented together. It's not only about "structure api interface", but it:
- introduces use of
nim-brokersat the API level - brings
gitnexusand updates agents instructions - adds
onelogosdeliverytarget (although as a demo) - Change
WakuMode - Renames events
- Changes Reliable Channels API
- etc.
These are all reasonable changes. But:
- Because e.g. there's no agreement regarding use of
nim-brokers, all other changes are hold and not merged. - Yet it's dead weight for this specific PR. It's fog for reviewers. And my browser can barely handle it 😄
Nobody loves long-living PRs.
Extracting these changes into smaller PRs would definitely increase the overall review speed.
There was a problem hiding this comment.
Is there an example how would the use of this API look in Nim?
Important notice
This PR is a draft and issued for early examination and for open discussions in the team.Do expect changes, new commits!
UPDATE
While the aim is still initiate discussion about the approach for long term, the PR reached state where it is mature enough even to be merged (as soon ci is green)
Important this PR not intended to change any behavior, nor re-wire all and everything.
It introduces a concept that allows us greater flexibility for later - smaller steps - refactorings.
This PR does not change any FFI facing API, nor adding new. All code changes out of ./logos_delivery/api/ folder is just to match the new interface requirements - with minimalist approach.
Notes to the reviewers
Although the PR seems extensive and touching lot of stuff, please focus on the followings.
API types and interfaces:
API implementation locations:
WakuMode-noModeenum removed and exercised viaOption[WakuMode]to enable single WakuMode shape everywhere (cli and API)examples/cpp/waku/cpp- it was not platform independent before)Description
This PR aims to showcase a desired API shape that matches our new layering of logos-delivery.
This POC uses latest nim-brokers that allows write natural Nim API interfaces and implementation.
The currently pinned version is an arbitrary commit - not a fixed version - due to need of fixes in the library to support fully all API shapes.
It allows us to turn current existing structures to be consumed over the interfaces.
Comes with easy mock-able interface classes- even per single API request level.
Currently target
make onelogosdeliveryexists only to validate compilation from source ./logos_delivery.nim - the new library root. It can be removed later as it does not produce any useful binary today.Changes
Based on the latest code restructuring, and proper nim library format, the new API is placed at
./logos_delivery/apiIt introduces exacting layer matching interfaces:
Implementation wiring:
Each interface is implemented in the right layer spot:
As a temporal solution the POC introduces a new target (make + nimble) =>
onelogosdeliveyIt builds with simple:
make onelogosdelivery=>builds
Changes to existing codebase
WakuMode- cli_args now useOption[WakuMode]instead of having a bit controversialnoModeelement.ReliableChannelManagerandReliableChanneldo not use sendHandler but instead utilize theMessagingClientInterfaceto issue lower layer send API call.LogosDeliveryimplementation class is not the master / orchestrator class of the library (both for nim and ffi users)getNodeInfo,getAvailableConfigs)Issue
closes #3956