#ifndef SWARMD_CLIENT_H
#define SWARMD_CLIENT_H
// A C++ client for swarmd. docs/SWARMD-DESIGN.md §7 item 7, §9 step 5.
//
// "Any program in C++" is the goal, and until now the only clients were a
// Python test harness and one 250-line tool. Both hand-assembled frames.
//
// ⛔ NOTHING HERE IS HAND-WRITTEN WIRE, and that is the entire reason this file
// exists rather than a page of documentation telling each consumer how to do it
// themselves. Two clients in this repo have already been wrong that way — the
// first channel gate hand-rolled compact-encoding in Python and the daemon
// answered "undecodable message" to every frame, and swarmd-probe gave its sends
// an id range that overlapped the content calls and reported a fetch reply as a
// send result. Both are the same mistake: a client is a wire implementation, and
// there should be ONE.
//
// SHAPE. Setup calls block for their reply; everything the daemon says on its
// own arrives through callbacks during poll(). That split is not arbitrary:
// channel_open has to return a handle before you can send on it, while a peer
// arriving has nobody waiting for it.
//
// ⛔ AND A BLOCKING CALL STILL DISPATCHES EVENTS. Frames do not queue politely
// behind the reply you asked for: a peer can come up, or a 100 MB fetch can
// finish, while you are waiting for the answer to channel_open. Anything read on
// the way to a reply is delivered, not dropped and not deferred.
//
// THREADING. One Client belongs to one thread. It owns a socket and a buffer and
// guards neither.
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <string>
#include <vector>
namespace swarmd {
// What the daemon says on its own. Every one of these arrives inside poll(), or
// inside a blocking call that read it on the way to a reply.
struct ClientCallbacks {
// A frame from a peer, on a channel this client opened.
std::function<void(uint64_t channel, uint64_t slot, uint64_t origin_kind,
uint64_t origin_id, const uint8_t *bytes, size_t len)> on_message;
std::function<void(uint64_t peer, const uint8_t pubkey[32])> on_peer_up;
std::function<void(uint64_t peer)> on_peer_down;
// The channel has drained and will accept writes again.
std::function<void(uint64_t channel)> on_drain;
std::function<void(const uint8_t hash[32], uint64_t received, uint64_t total)> on_content_progress;
std::function<void(const uint8_t hash[32], const std::string &path)> on_content_done;
std::function<void(const uint8_t hash[32], const std::string &reason)> on_content_failed;
// The daemon went away, or sent something this client could not read. After
// this the Client is closed; connect() again to resume.
std::function<void(const std::string &reason)> on_disconnect;
};
struct Status {
uint64_t dht_state = 0; // 0 starting, 1 connecting, 2 up
bool bootstrapped = false;
uint64_t peers_known = 0;
uint64_t peers_connected = 0;
uint64_t connecting = 0;
uint64_t queued = 0;
uint64_t local_clients = 0;
};
class Client {
public:
Client() = default;
~Client();
Client(const Client &) = delete;
Client &operator=(const Client &) = delete;
ClientCallbacks on;
// Dial the daemon and say HELLO. `name` and `app_identity` are what the
// daemon records for this client; app_identity is what echo suppression and
// membership will key on, so a program that has one should pass it.
bool connect(const std::string &socket_path, const std::string &name,
const uint8_t app_identity[32] = nullptr, int timeout_ms = 5000);
void close();
bool connected() const { return _fd >= 0; }
uint64_t client_id() const { return _client_id; }
// Every byte the daemon has written to us. This is the number that says
// bulk content did NOT come over the socket (design §6), so it belongs to
// the client rather than to whoever remembers to count.
uint64_t bytes_in() const { return _bytes_in; }
const uint8_t *daemon_pubkey() const { return _daemon_pubkey; }
const std::string &last_error() const { return _err; }
// Serve/receive on a topic. Both flags default true, which is what a peer
// that wants to meet anyone on the topic wants.
bool join(const uint8_t topic[32], bool server = true, bool client = true,
int timeout_ms = 5000);
bool leave(const uint8_t topic[32], int timeout_ms = 5000);
// Returns a channel handle, or 0. `slots` is how many message types the
// channel carries.
uint64_t open_channel(const std::string &protocol,
const std::vector<uint8_t> &id = {},
uint32_t slots = 1, int timeout_ms = 5000);
bool close_channel(uint64_t channel, int timeout_ms = 5000);
// ⛔ `accepted` FALSE MEANS THE FRAME WAS NOT TAKEN — retry, do not assume.
// The daemon refuses when the handle is unknown or the channel is open on no
// peer, which is every moment before the remote opens its side. `writable`
// says whether more will be accepted right now; on false, wait for on_drain.
// `target` 0 means every peer holding this channel.
bool send(uint64_t channel, uint32_t slot, uint64_t target,
const uint8_t *bytes, size_t len, bool *writable = nullptr,
int timeout_ms = 5000);
bool status(Status &out, int timeout_ms = 5000);
// ---- content ----
// The overlay's hash of a file the DAEMON can read — blake2b over the
// hypercore leaf construction. A hash from anywhere else is one the overlay
// will reject, which is why this asks the daemon rather than computing it.
bool content_hash(const std::string &path, uint8_t out_hash[32], int timeout_ms = 30000);
bool content_serve(const uint8_t hash[32], const std::string &path, int timeout_ms = 5000);
// Returns once the fetch has STARTED. Watch on_content_done / _failed.
bool content_fetch(const uint8_t hash[32], const std::string &dest_path, int timeout_ms = 5000);
bool content_cancel(const uint8_t hash[32], bool discard, int timeout_ms = 5000);
// Read whatever is there and dispatch it. Returns false once disconnected.
// `timeout_ms` 0 polls without waiting.
bool poll(int timeout_ms);
private:
// Send one encoded request. Takes ownership of `buf` exactly as the
// generated encoders hand it over: heap-allocated, caller frees.
bool _send_frame(uint8_t *buf, size_t len);
// Pump until the response to `id` lands, dispatching events on the way.
// `body`/`body_len` point into `_reply` and are valid until the next pump.
bool _await(uint64_t id, int timeout_ms, const uint8_t **body, size_t *body_len);
bool _read_some(int timeout_ms);
// Consume whole frames from `_in`. Returns true if the awaited id landed.
bool _drain_frames(uint64_t await_id, const uint8_t **body, size_t *body_len);
void _dispatch_event(const void *msg);
void _fail(const std::string &why);
int _fd = -1;
uint64_t _next_id = 1;
uint64_t _client_id = 0;
uint8_t _daemon_pubkey[32] = { 0 };
std::string _err;
uint64_t _bytes_in = 0;
std::vector<uint8_t> _in; // partial inbound
std::vector<uint8_t> _reply; // the frame a blocking call is returning
};
} // namespace swarmd
#endif