API — ICoreEssentials/Network
The public contract of 15 header(s) under ICoreEssentials/Network — 22 class/struct definition(s), 198 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
ICoreHostAddress.h#
ICoreEssentials/Network/ICoreHostAddress.h
ICoreHostAddress -- an IPv4 or IPv6 address; ICoreHostLookup -- names to addresses.
auto a = ICoreHostAddress::parse("2001:db8::1"); a->toString(); // "2001:db8::1" a->isLoopback(); // false
std::string error; auto all = ICoreHostLookup::lookup("example.com", &error);
parse() is strict: IPv4 must be four decimal parts 0..255 with no leading zeros ("010.0.0.1" is refused rather than read as octal or decimal); IPv6 follows RFC 4291, "::" compression and an embedded IPv4 tail included, with an optional numeric zone ("fe80::1%4"). toString() writes the RFC 5952
ICoreHostAddress#
ICoreHostAddress.h:43 · class · 12 declaration(s)
Opened by row PS5.20 of the Platform SDK product plan.
class ICoreHostAddress {
public:
enum class Family {
None = 0,
IPv4 = 4,
IPv6 = 6
};
// Family::None, the "no address" value.
ICoreHostAddress() noexcept;
[[nodiscard]] static std::optional<ICoreHostAddress> parse(std::string_view text);
// From the raw bytes in network order: 4 for IPv4, 16 for IPv6.
[[nodiscard]] static std::optional<ICoreHostAddress> fromBytes(const ICoreByteArray& bytes);
[[nodiscard]] static ICoreHostAddress fromIPv4(std::uint32_t hostOrder);
[[nodiscard]] static ICoreHostAddress loopback(Family family);
[[nodiscard]] static ICoreHostAddress any(Family family);
[[nodiscard]] Family family() const noexcept;
[[nodiscard]] bool isNull() const noexcept;
[[nodiscard]] std::string toString() const;
[[nodiscard]] ICoreByteArray toBytes() const;
// The IPv4 address, for an IPv4 address or an IPv4-mapped IPv6 one.
[[nodiscard]] std::optional<std::uint32_t> toIPv4() const noexcept;
// The IPv6 zone (scope id); 0 when there is none.
[[nodiscard]] std::uint32_t scopeId() const noexcept;
[[nodiscard]] bool isLoopback() const noexcept; // 127/8, ::1
[[nodiscard]] bool isAny() const noexcept; // 0.0.0.0, ::
[[nodiscard]] bool isLinkLocal() const noexcept; // 169.254/16, fe80::/10
[[nodiscard]] bool isPrivate() const noexcept; // RFC 1918, fc00::/7
[[nodiscard]] bool isMulticast() const noexcept; // 224/4, ff00::/8
friend bool operator==(const ICoreHostAddress& a, const ICoreHostAddress& b) noexcept;
friend bool operator!=(const ICoreHostAddress& a, const ICoreHostAddress& b) noexcept;
friend bool operator<(const ICoreHostAddress& a, const ICoreHostAddress& b) noexcept;
};
ICoreHostLookup#
ICoreHostAddress.h:89 · class · 3 declaration(s)
class ICoreHostLookup {
public:
ICoreHostLookup() = delete;
using Done = std::function<void(std::vector<ICoreHostAddress> addresses, std::string error)>;
// Every address `hostName` resolves to, IPv6 and IPv4, duplicates removed
// and in the resolver's order. Empty on failure, with the reason in
// `*error` when `error` is not null.
[[nodiscard]] static std::vector<ICoreHostAddress> lookup(std::string_view hostName, std::string* error);
// The same, answered on the main thread. `done` always runs exactly once.
static void lookupAsync(std::string hostName, Done done);
};
};
ICoreHttpClient.h#
ICoreEssentials/Network/ICoreHttpClient.h
⚠⚠ QT-FREE AGAIN, A10.6 (2026-09-11). The 2026-09-10 main merge took this header WHOLESALE from main, and main's copy still had the five Qt includes A9.10 removed on 2026-08-22 -- plus a QByteArray return on readAll() and ICoreHttpRequest::rawHeader(), a QNetworkReply* private constructor and a
QNetworkReply* qt()with zero callers. main's API is kept WHOLE (get(), send(), download(), setRetryPolicy(), setTotalTimeout(), the reply's rawHeader()/timedOut()/startTotalTimeout()); only the Qt spellings went, back to the ones ui-swap had already paid for. Every caller was re-read first: all of them feed readAll() to toStdString() or keep an ICoreByteArray, and none named qt(). ICoreUrl.h names neither QNetworkRequest nor QUrl in code, so the "the other three STAY" note that stood here was wrong on its own evidence (W9.10 measured that once already).The seats: NSURLSession on Apple (Backends/Native/Apple/ICoreHttpClient.mm),
ICoreHttpRequest#
ICoreHttpClient.h:106 · class · pImpl · 15 declaration(s)
the request ------------------------------------------------------------
class ICoreHttpRequest {
public:
ICoreHttpRequest();
~ICoreHttpRequest();
// A request is a small value and WAS implicitly copyable (it held a
// QNetworkRequest, which is); a unique_ptr member deletes that, so the four
// are written out in the .cpp rather than silently narrowing the contract.
ICoreHttpRequest(const ICoreHttpRequest& other);
ICoreHttpRequest& operator=(const ICoreHttpRequest& other);
ICoreHttpRequest(ICoreHttpRequest&& other) noexcept;
ICoreHttpRequest& operator=(ICoreHttpRequest&& other) noexcept;
// ICoreUrl went Qt-free, so the parse happens in this wrapper now rather
// than being carried in. It is the layer's only remaining claimant of
// <QUrl>, and it is the right place for it: a QNetworkRequest is the one
// thing in the project that genuinely needs a parsed URL, and it is one
// line away -- a line that now sits in the .cpp.
// Was url.text(), a private by-reference accessor reached through
// friendship. H2.17 put ICoreUrl's text behind an Impl and deleted both;
// toString() is the same value, one ICoreString copy dearer.
void setUrl(const ICoreUrl& url);
// The text of the URL. See the design note on why this is not an ICoreUrl.
ICoreString url() const;
// Content-Type. Named rather than enumerated -- see the narrowing note.
void setContentType(const ICoreString& type);
ICoreString contentType() const;
void setRawHeader(const ICoreByteArray& name, const ICoreByteArray& value);
// ⚠ RETURNED QByteArray UNTIL A9.10 (2026-08-22), and again in main's copy
// until A10.6. A native seat has nothing Qt to return, and no call site
// wanted the Qt type.
[[nodiscard]] ICoreByteArray rawHeader(const ICoreByteArray& name) const;
// INACTIVITY timeout, not a total-request cap: Qt restarts it whenever
// bytes arrive. The one caller depends on that distinction -- a long
// agentic turn streams for minutes and a total cap would kill it mid-answer.
void setTransferTimeout(int msecs);
// A cap on ONE ATTEMPT, wall clock, whether or not bytes are arriving --
// the other half of the pair above, added for the licence client
// the account manager. A copilot turn wants the inactivity timer; a
// request that decides whether the editor window opens wants a number it
// cannot exceed, because "the server is answering one byte a second" and
// "the server is down" look identical to an inactivity timer and only one
// of them is worth waiting for.
//
// 0 (the default) means no cap. When it fires the attempt is abort()ed and
// the reply reports timedOut(); under send() that attempt may then be
// retried, so the caller's real bound is attempts x (cap + backoff).
void setTotalTimeout(int msecs);
int totalTimeout() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreHttpReply#
ICoreHttpClient.h:174 · class · pImpl · 15 declaration(s)
the reply --------------------------------------------------------------
class ICoreHttpReply {
public:
// ⚠⚠ THE CONTRACT, DECIDED 2026-08-27 -- Linux backend plan L9.16. A reply
// MAY OUTLIVE ITS CLIENT, and destroying it afterwards must not crash. The
// native seats back the reply with shared state the client does not own
// outright. The Qt seat holds a QPointer<QNetworkReply> for this reason --
// main's copy of that seat had gone back to a raw pointer, which the
// client's QNetworkAccessManager deletes out from under it; A10.6 restored
// the QPointer.
//
// Deletes the reply unless deleteLater() already released it -- B7's
// ICoreProcess destructor, for the same reason.
~ICoreHttpReply();
// Owns an in-flight request; copying one is meaningless and nothing moves
// one, so both are deleted rather than defined.
ICoreHttpReply(const ICoreHttpReply&) = delete;
ICoreHttpReply& operator=(const ICoreHttpReply&) = delete;
// It DRAINS: bytes returned once are not returned again, so a streaming
// caller reads on every onReadyRead and once more after onFinished.
[[nodiscard]] ICoreByteArray readAll();
// Drives finished(). Cancellation goes through here rather than through
// destruction, because the caller wants the settle handler to run.
void abort();
// See the narrowing note: the code itself is never read, only its presence
// and the message.
bool hasError() const;
ICoreString errorString() const;
// 0 when the response never got far enough to have one.
int httpStatus() const;
// A RESPONSE header, empty when absent. The request has had a rawHeader()
// since B14; the reply had none, and a resumable download cannot work
// without one -- `Content-Range` is the only thing that says whether the
// server honoured a `Range` request, and Qt reports a range it silently
// IGNORED as an ordinary 200 carrying the whole file. Without this the
// downloader would append a full copy onto a partial one and hand the
// verifier a file that is the right length nowhere and fails its digest
// with no way to tell why. Rule 3: the capability went into the wrapper.
[[nodiscard]] ICoreByteArray rawHeader(const ICoreByteArray& name) const;
// True when ICoreHttpRequest::setTotalTimeout()'s cap fired and aborted
// this attempt. hasError() is true either way; this is what separates "we
// gave up" from "the peer refused", which are different sentences in a UI
// and different decisions in a retry.
bool timedOut() const;
// Step one of the two-step -- see the ownership note. Releases the reply to
// the event loop and drops this wrapper's pointer, which is what makes
// destroying the wrapper from inside a signal handler safe.
//
// ⚠ AND THE WRAPPER GOES INERT: after this, httpStatus() reads 0, hasError()
// false, readAll()/rawHeader()/errorString() empty. That is what the Qt seat
// always did (its pointer is null), and the download path reads a released
// reply when a cancel lands inside a retry backoff.
void deleteLater();
// --- asynchronous results (PHASE 2) -------------------------------------
//
// The same addition ICoreProcess just took, for the same reason: without
// it a caller cannot learn that bytes arrived except through qt(), and so
// has to keep a QObject base purely to be a connect context. Read that
// header's phase-2 note for the delivery argument -- it applies verbatim,
// with the reply as its own context object, so handlers still run
// synchronously inside the emission on the reply's own thread.
//
// Registering twice replaces rather than adds; these are callbacks.
// Bytes are available. Streaming callers read them with readAll() and may
// be called many times before onFinished.
void onReadyRead(std::function<void()> fn);
// The exchange is over -- successfully, in error, or because abort() was
// called. Ask hasError()/httpStatus() which of those it was.
void onFinished(std::function<void()> fn);
// Arms the total cap on this reply, aborting it after msecs whether or not
// bytes are arriving. Every entry point on the client already arms it from
// ICoreHttpRequest::setTotalTimeout(), so a caller needs this only for a
// reply it obtained some other way. Arming twice starts a second clock;
// both will fire and the first one wins.
void startTotalTimeout(int msecs);
// Stop delivering, permanently. This is what a caller's old
// `qt()->disconnect(this)` meant, and it is safe to call from inside a
// handler -- which is exactly where the settle path calls it.
void clearCallbacks();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreHttpDownloadResult#
ICoreHttpClient.h:308 · struct · 2 declaration(s)
What one download() ended as.
struct ICoreHttpDownloadResult {
public:
enum class Outcome {
Ok, // the transfer completed; the bytes are on disk
Refused, // the server answered, and not with a success status
Unreachable, // no answer at all: no network, DNS, refused, timed out
Interrupted, // it started and stopped short of the announced length
WriteFailed, // the destination could not be opened, or a write failed
Aborted // cancel(), or the handle was destroyed
};
Outcome outcome = Outcome::Unreachable;
// 0 when no response line ever arrived. ⚠ 403 IS THE INTERESTING ONE for
// the updater: a presigned R2 URL that has expired answers 403, and that
// is a request for a fresh grant, not a failure to report to anyone.
int httpStatus = 0;
std::int64_t bytesOnDisk = 0; // the destination's size NOW
std::int64_t bytesThisCall = 0; // how many bytes THIS call added
std::int64_t expectedBytes = -1; // the whole resource, -1 when unknown
bool resumed = false; // it continued a file that was already there
bool restarted = false; // a partial file was discarded and refetched from 0
int attempts = 0;
// Diagnostics. Names hosts, paths and timeouts, so never shown to a user.
ICoreString error;
[[nodiscard]] bool ok() const;
// Stable, English, not localised.
[[nodiscard]] static ICoreString describe(Outcome outcome);
};
};
ICoreHttpDownload#
ICoreHttpClient.h:357 · class · pImpl · 8 declaration(s)
The handle for a download in flight: progress, cancellation, and the file.
class ICoreHttpDownload {
public:
~ICoreHttpDownload();
// Owns an in-flight transfer and an open file; copying one is meaningless.
ICoreHttpDownload(const ICoreHttpDownload&) = delete;
ICoreHttpDownload& operator=(const ICoreHttpDownload&) = delete;
// Stops the transfer. The finished callback still runs, once, with
// Outcome::Aborted -- a caller that cancels usually still has cleanup to
// do, and silence would be a leak of whatever it was waiting on. Calling
// it after the download has finished does nothing.
void cancel();
[[nodiscard]] bool isFinished() const;
// What the destination holds right now. Answers during the transfer.
[[nodiscard]] std::int64_t bytesOnDisk() const;
// -1 until the server says, which is after the first response header.
[[nodiscard]] std::int64_t expectedBytes() const;
[[nodiscard]] ICoreString destinationPath() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreHttpClient#
ICoreHttpClient.h:395 · class · pImpl · 8 declaration(s)
the client -------------------------------------------------------------
class ICoreHttpClient {
public:
ICoreHttpClient();
~ICoreHttpClient();
// Holds an event-loop object; copying one is meaningless.
ICoreHttpClient(const ICoreHttpClient&) = delete;
ICoreHttpClient& operator=(const ICoreHttpClient&) = delete;
enum class Method { Get, Post };
// Streaming: the caller owns the reply and drives it with the callbacks.
// This is the copilot's shape -- bytes arrive for a minute and are consumed
// as they land -- and neither the total cap nor the retry policy applies to
// it, because both need to own the reply to do their job.
std::unique_ptr<ICoreHttpReply> post(const ICoreHttpRequest& request,
const ICoreByteArray& body);
// GET. Was deliberately absent under Rule 3 until something asked for it.
// Something asked: a client that fetches a small JSON document.
std::unique_ptr<ICoreHttpReply> get(const ICoreHttpRequest& request);
// --- streamed to a file, resumable, retrying ---------------------------
//
// GETs `request`'s URL into `destinationPath`, writing bytes as they
// arrive rather than buffering the body. Returns immediately; the handle
// drives it and the callbacks arrive on the event loop.
//
// RESUME IS AUTOMATIC AND IS NOT OPTIONAL. If the destination already
// holds bytes, the request carries `Range: bytes=<n>-` and the bytes are
// appended. So an interrupted 400 MB download costs its remainder, not
// itself, and -- the reason this matters for the updater -- a presigned
// URL that expires mid-transfer costs one re-grant rather than the whole
// artifact. The caller re-grants and calls download() again with the new
// URL and the SAME destination; the file on disk is the state.
//
// ⚠ A SERVER MAY IGNORE `Range` AND ANSWER 200 WITH THE WHOLE FILE, and
// appending that to a partial one produces a corrupt file of plausible
// length. That case is detected from the status and the partial file is
// TRUNCATED first; the result says `restarted`.
//
// RETRY reuses setRetryPolicy(), and composes with resume: each attempt
// re-reads what is on disk and continues from there, so three attempts of
// a flaky connection make forward progress rather than three false starts.
// ⚠ 401/403/404/410 are NOT retried -- they mean this URL is dead, and
// hammering it cannot help. That is the caller's signal to re-authorise.
//
// TIMEOUTS: use ICoreHttpRequest::setTransferTimeout() (inactivity) for a
// download, not setTotalTimeout(). A total cap is honoured if set, but it
// caps ONE ATTEMPT -- on a large artifact it becomes a chunk size rather
// than a deadline, because the retry that follows resumes.
std::unique_ptr<ICoreHttpDownload> download(
const ICoreHttpRequest& request,
const ICoreString& destinationPath,
std::function<void(std::int64_t bytesOnDisk, std::int64_t expectedBytes)> onProgress,
std::function<void(const ICoreHttpDownloadResult&)> onFinished);
// --- buffered, capped, retrying ----------------------------------------
//
// The other shape: one small request, one small answer, and the caller
// wants the outcome rather than the stream. The client keeps the reply,
// applies the request's total cap to each attempt, retries what is worth
// retrying, and calls onSettled ONCE with the reply of the final attempt.
//
// The reply is still alive inside the callback -- httpStatus(), readAll(),
// hasError(), errorString() and timedOut() all answer -- and is destroyed
// after it returns. Do not keep the reference.
//
// Nothing here blocks: send() returns immediately and the callback arrives
// on the event loop, which is the only shape a licensing or telemetry call
// is allowed to have.
void send(Method method, const ICoreHttpRequest& request, const ICoreByteArray& body,
std::function<void(ICoreHttpReply&)> onSettled);
// How many attempts ONE send() may make (1 = no retry, the default) and the
// wait before the second, doubled for each attempt after it.
//
// ⚠ WHAT IS RETRIED DEPENDS ON THE VERB, and the asymmetry is not
// conservatism for its own sake. A GET is idempotent, so any transport
// failure or 5xx is retried. A POST may ALREADY HAVE BEEN EXECUTED when the
// answer went missing -- retrying an activation that actually succeeded
// burns a second seat, and the user cannot tell you that happened. So a
// POST is retried only when the connection was never established
// (refused / host not found / proxy refused) or the server explicitly asked
// it to wait (429, 503). A 500 or a dropped connection mid-answer is
// reported, not repeated.
void setRetryPolicy(int attempts, int initialBackoffMsecs);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreHttpRetryPolicy.h#
ICoreEssentials/Network/ICoreHttpRetryPolicy.h
THE HTTP RETRY AND RESUME POLICY, ONCE.
L9.65 of the Linux backend plan. Every HTTP seat in this tree had its own copy of this decision table: the NSURLSession seat (A10.6), the WinHTTP seat (W10.29) and the libcurl seat (L9.60) -- four copies until LQ.13 deleted main's Qt client on 2026-09-17, three after.
⚠⚠ THE COPIES WERE NOT "SIMILAR", THEY WERE BYTE-IDENTICAL WHERE IT COUNTS. Measured across all three before this file existed: serverAskedToWait, transientForIdempotent and urlIsDead were the same three functions CHARACTER FOR CHARACTER, and worthRetrying() was the same table in the same order. What differed was only how each seat READ ITS OWN TRANSPORT -- a StreamPtr and an errorCode on libcurl, an ExchangePtr and a neverEstablished flag on WinHTTP.
ICoreHttpRetryFacts#
ICoreHttpRetryPolicy.h:35 · struct · 0 declaration(s)
What a seat knows about one finished attempt.
struct ICoreHttpRetryFacts {
public:
// --- the attempt counter, from setRetryPolicy(attempts, backoff) --------
int attemptsMade = 0;
int maxAttempts = 1;
// The seat's own "did this end badly" verdict. A seat that cannot tell
// must say false: this policy never guesses on its behalf.
bool hasError = false;
// 0 when no response line was read at all -- which is the case every
// "never established" rule below turns on. Otherwise the HTTP status.
int httpStatus = 0;
// GET and HEAD are idempotent; POST is not, and that is the whole reason
// the two branches differ. Named for the PROPERTY rather than the verb so
// a seat that grows PUT/DELETE does not have to edit this header.
bool isIdempotent = false;
// ⚠ THE TOTAL-REQUEST CAP IS NOT A TRANSPORT ERROR AND MUST NOT READ AS
// ONE. setTotalTimeout() fires while the request may already have been
// EXECUTED by the server -- we stopped listening, the work may still have
// happened -- so a fired cap disqualifies a POST retry even though the
// connection looks unused.
bool totalCapFired = false;
// The transport is certain no byte of the request reached a server: DNS
// failed, connect() failed, TLS never completed. The ONLY safe repeat for
// a non-idempotent method.
bool connectionNeverEstablished = false;
};
};
ICoreHttpDownloadRetryFacts#
ICoreHttpRetryPolicy.h:69 · struct · 0 declaration(s)
What a seat knows about one finished DOWNLOAD attempt.
struct ICoreHttpDownloadRetryFacts {
public:
int attemptsMade = 0;
int maxAttempts = 1;
// The three states in which a retry is not this policy's business at all,
// whatever the wire said.
bool cancelled = false;
bool detached = false;
bool writeFailed = false;
bool hasError = false;
int httpStatus = 0;
// Negative when the server announced no length. Resume cannot be decided
// without it, so an unknown length is never a short read.
std::int64_t expectedBytes = -1;
std::int64_t bytesOnDisk = 0;
};
};
ICoreLocalServer.h#
ICoreEssentials/Network/ICoreLocalServer.h
ICoreLocalServer -- accepts ICoreLocalSocket connections from processes on the same machine.
ICoreLocalServer server; server.setNewConnectionHandler([&] { while (auto socket = server.nextPendingConnection()) adopt(std::move(socket)); }); if (!server.listen("com.example.editor")) fail(server.errorString());
A NAME is either a plain name or a path. A plain name ("com.example.editor") becomes a socket file in the user's own temporary directory -- $XDG_RUNTIME_DIR on Linux when it is set, the system temporary directory otherwise -- and serverPath() says which file. A name containing a path separator is used as the path itself. The file is readable and writable by
ICoreLocalServer#
ICoreLocalServer.h:47 · class · pImpl · 15 declaration(s)
Opened by row PS5.21 of the Platform SDK product plan.
class ICoreLocalServer {
public:
ICoreLocalServer();
// Stops listening. Sockets already handed out are the caller's and stay
// connected; ones still pending are closed.
~ICoreLocalServer();
ICoreLocalServer(const ICoreLocalServer&) = delete;
ICoreLocalServer& operator=(const ICoreLocalServer&) = delete;
bool listen(const std::string& name);
[[nodiscard]] bool isListening() const noexcept;
// The socket file this server is bound to; empty when not listening.
[[nodiscard]] std::string fullServerName() const;
[[nodiscard]] bool hasPendingConnections() const;
// The oldest accepted connection, now owned by the caller; null when none
// is waiting.
[[nodiscard]] std::unique_ptr<ICoreLocalSocket> nextPendingConnection();
// Blocks until a connection is pending (true) or the timeout passes.
bool waitForNewConnection(int timeoutMs = 3000);
void setNewConnectionHandler(std::function<void()> handler);
void close();
[[nodiscard]] std::string errorString() const;
// The socket file a name stands for (see above). Pure: nothing is created.
[[nodiscard]] static std::string serverPath(const std::string& name);
// Removes the socket file for `name` if no server is answering on it.
// False when a live server holds it or the file cannot be removed.
static bool removeServer(const std::string& name);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreLocalSocket.h#
ICoreEssentials/Network/ICoreLocalSocket.h
ICoreLocalSocket -- a byte stream to another process on the same machine.
ICoreLocalSocket socket; if (!socket.connectToServer("com.example.editor")) fail(socket.errorString()); socket.setReadyReadHandler([&] { handle(socket.readAll()); }); socket.write(request);
The other end is an ICoreLocalServer listening under the same name (see ICoreLocalServer.h for how a name becomes an address). It is a Unix domain socket on every platform that has one: macOS, Linux, iPadOS, Android, and Windows 10 1803 and later, whose AF_UNIX sockets the WinUI floor (1809) guarantees -- so all of them share one implementation. A browser has none: connectToServer() fails and errorString() says so.
ICoreLocalSocket#
ICoreLocalSocket.h:42 · class · pImpl · 19 declaration(s)
Opened by row PS5.21 of the Platform SDK product plan.
class ICoreLocalSocket {
public:
ICoreLocalSocket();
// Closes the connection; unsent bytes are discarded (waitForBytesWritten()
// first to avoid that). No handler runs after the destructor starts.
~ICoreLocalSocket();
ICoreLocalSocket(const ICoreLocalSocket&) = delete;
ICoreLocalSocket& operator=(const ICoreLocalSocket&) = delete;
// Connects to the server listening under `name`. Local connections are
// made or refused at once, so this returns at once.
bool connectToServer(const std::string& name);
[[nodiscard]] bool isConnected() const;
// The address this socket connected to (ICoreLocalServer::serverPath()),
// or empty for a socket an ICoreLocalServer accepted.
[[nodiscard]] std::string serverPath() const;
// Queues all of `data`; returns data.size(), or -1 when not connected.
std::int64_t write(const ICoreByteArray& data);
[[nodiscard]] std::int64_t bytesToWrite() const;
// Blocks until everything written has been handed to the OS, the
// connection drops, or `timeoutMs` passes. A negative timeout waits
// for as long as it takes.
bool waitForBytesWritten(int timeoutMs = 3000);
[[nodiscard]] std::int64_t bytesAvailable() const;
ICoreByteArray read(std::int64_t maxBytes);
ICoreByteArray readAll();
// Blocks until at least one byte is available (true), or the connection
// drops with nothing left to read, or the timeout passes (false).
bool waitForReadyRead(int timeoutMs = 3000);
// Blocks until the peer has closed (true) or the timeout passes.
bool waitForDisconnected(int timeoutMs = 3000);
void setReadyReadHandler(std::function<void()> handler);
// Runs once, after the last ready-read of the connection, when the peer
// closes or the connection fails. Not run for disconnectFromServer().
void setDisconnectedHandler(std::function<void()> handler);
// Closes now; unsent bytes are discarded. The socket can connect again.
void disconnectFromServer();
[[nodiscard]] std::string errorString() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreOnDemandAssets.h#
ICoreEssentials/Network/ICoreOnDemandAssets.h
ICoreOnDemandAssets#
ICoreOnDemandAssets.h:45 · class · 8 declaration(s)
ICoreOnDemandAssets -- files a program needs SOMETIMES, fetched the first time they are needed rather than shipped in the first download.
class ICoreOnDemandAssets {
public:
// (ok, why): why is empty when ok is true.
using Done = std::function<void(bool ok, const std::string& why)>;
// Where assets live in the file system. In a browser "/assets" -- memory,
// NOT under ICorePersistence::root(): the browser's HTTP cache already keeps
// the bytes, and a copy in IndexedDB would be a second cache to invalidate.
// On a desktop, empty until setRoot().
[[nodiscard]] static std::filesystem::path root();
static void setRoot(const std::filesystem::path& root);
// Where they are fetched from; a name is appended to it. In a browser
// "assets/", which the browser resolves against the page -- the layout
// icore_web_page() packages. On a desktop, empty: nothing is fetched.
[[nodiscard]] static std::string baseUrl();
static void setBaseUrl(const std::string& url);
// Where `name` is (or will be) in the file system. Does not fetch. Empty for
// a name that is refused.
[[nodiscard]] static std::filesystem::path localPath(const std::string& name);
// True when `name` is in the file system now, complete.
[[nodiscard]] static bool isReady(const std::string& name);
// Make `name` present, then call `done` -- see the header note on when.
static void ensure(const std::string& name, Done done);
// How many fetches have STARTED since the program began. For rigs: a second
// ensure() of a ready asset must not move it.
[[nodiscard]] static int fetchesStarted();
};
ICoreTcpConnection.h#
ICoreEssentials/Network/ICoreTcpConnection.h
ICoreTcpConnection#
ICoreTcpConnection.h:39 · struct · 2 declaration(s)
One live connection, shared between the server that owns it, the poller thread that reads it, and every ICoreTcpSocket handle that names it.
struct ICoreTcpConnection {
public:
#ifdef ICORE_OS_WINDOWS
using Handle = SOCKET;
static constexpr Handle kInvalid = INVALID_SOCKET;
#else
using Handle = int;
static constexpr Handle kInvalid = -1;
#endif
std::mutex mutex;
// --- guarded by mutex ---------------------------------------------------
Handle socket = kInvalid;
// Filled by the poller thread, taken by readAll() on the GUI thread.
ICoreByteArray pending;
// The half-arrived request the call site parks here between two readyRead
// deliveries. Storage on the CONNECTION, never in the handle -- see the
// handle's header.
ICoreByteArray parked;
bool connected = false;
// One readyRead notification in flight at a time: a client sending a
// request in three packets should cost one callback per batch, not three.
bool notifyQueued = false;
// Delivered exactly once, and only if the connection was ever connected.
bool disconnectedDelivered = false;
// ⚠ The handlers live HERE rather than on the handle, because the handle is
// a value that call sites copy into lambdas. Registering on a copy has to
// affect the connection, or a call site that stored a copy would stop being
// called back for reasons it could not see.
std::function<void()> onReadyRead;
std::function<void()> onDisconnected;
};
};
ICoreTcpServer.h#
ICoreEssentials/Network/ICoreTcpServer.h
⚠ THE THREE QT INCLUDES THAT STOOD HERE ARE GONE (A9.10, 2026-08-22). All three were held by ONE member: <QTcpServer> was qt()'s return type, <QTcpSocket> its neighbour, and <QtGlobal> supplied the quint16 that listenOnLoopback() and port() spelled. qt() had no callers -- onNewConnection() replaced the five that existed -- so removing it took the first two with it, and the port type is std::uint16_t now, which is the same type by a name that belongs to nobody.
ICoreTcpServer#
ICoreTcpServer.h:71 · class · pImpl · 9 declaration(s)
class ICoreTcpServer {
public:
ICoreTcpServer();
// Takes every socket it accepted with it -- see the ownership note.
~ICoreTcpServer();
ICoreTcpServer(const ICoreTcpServer&) = delete;
ICoreTcpServer& operator=(const ICoreTcpServer&) = delete;
// Binds 127.0.0.1 on `port` (0 = let the OS choose). There is deliberately
// no way to bind anything else -- see the design note. `error` may be null.
bool listenOnLoopback(std::uint16_t port, ICoreString* error);
// The bound port, valid after a successful listenOnLoopback().
[[nodiscard]] std::uint16_t port() const;
[[nodiscard]] bool hasPendingConnections() const;
// A null handle when nothing is pending. The returned socket is owned by
// this server, not by the caller.
[[nodiscard]] ICoreTcpSocket nextPendingConnection();
// For connect() and disconnect() ONLY -- newConnection.
// --- the connect seam's replacement ---------------------------------------
//
// ⚠ THIS TIER HAD NO CALLBACK SURFACE AT ALL UNTIL A9.10, and qt() was the
// only way to learn that anything had happened -- which is why qt() is a
// link error naming itself on a native build rather than a seam: it hands
// out an object that does not exist there. Registering a hook is the
// question the five call sites were really asking (§0.28).
//
// Called on the thread that owns the loop, via the Process tier's
// dispatcher, exactly as ICoreProcess's callbacks are.
void onNewConnection(std::function<void()> fn);
// ⚠ qt() STOOD HERE AND IS GONE (A9.10, 2026-08-22). The paragraph above
// already said what it was: an accessor that "hands out an object that does
// not exist" on a native build, kept only because this tier had no other way
// to learn that anything had happened. onNewConnection() IS that other way,
// it has been landed since A9.10's callback surface, and qt() had no callers
// left anywhere in the tree. Removed rather than left declared-and-undefined
// -- that shape buys a seat time, it does not make the name free.
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreTcpSocket.h#
ICoreEssentials/Network/ICoreTcpSocket.h
⚠ THE THREE QT INCLUDES THAT STOOD HERE ARE GONE (A9.10, 2026-08-22), and the last of them outlived its reason by a row. <QByteArray> went when readAll() started returning ICoreByteArray; <QTcpSocket> and <QtGlobal> were qt()'s return type and its neighbours. qt() had no callers -- onReadyRead() and onDisconnected() replaced them -- so the accessor and all three includes go together, and this header names no Qt type at all.
ICoreTcpSocket#
ICoreTcpSocket.h:80 · class · pImpl · 16 declaration(s)
class ICoreTcpSocket {
public:
// A null handle. Exists because QList<ICoreTcpSocket> wants one and because
// nextPendingConnection() has to be able to say "nothing pending".
ICoreTcpSocket();
~ICoreTcpSocket();
// A HANDLE, copied by value into lambdas -- copying one aliases the same
// socket, it does not duplicate it. Written out because a unique_ptr<Impl>
// member deletes the implicit copy this type used to have.
ICoreTcpSocket(const ICoreTcpSocket& other);
ICoreTcpSocket& operator=(const ICoreTcpSocket& other);
[[nodiscard]] bool isNull() const noexcept;
// --- reading and writing -------------------------------------------------
// ⚠ RETURNED QByteArray UNTIL A9.10, and the change had to be REVERTED once
// before it could land. A Qt type in a return position is a link blocker,
// but sealing the header alone only pushed a Qt include DOWN into this
// tier's own .cpp: the toolkit dependency MOVED rather than left, and the
// running tally of files still reaching for Qt went UP by one here instead
// of down. The seal is free now only because the same change moves those
// bodies into Backends/Qt/, which that tally does not scan -- seal and move
// in ONE commit, which is what the reverted attempt taught.
//
// ⚠ The sentence above named the tally's file by path until 2026-08-22, and
// this comment is copied verbatim onto a generated page written for someone
// outside this tree, who cannot act on it. Say what the constraint IS here;
// leave the file that measures it to the pages that are about measuring.
//
// Both callers already assign straight into an ICoreByteArray
// (RunnerHttp.cpp:120-121), so not one of them wanted the Qt type.
ICoreByteArray readAll();
long long write(const ICoreByteArray& data);
// Pushes what is buffered onto the wire now. The SSE stream depends on this:
// an event the page never sees until the next one arrives is a stalled UI.
void flush();
// --- state and teardown --------------------------------------------------
// See the narrowing note -- the full state machine is never inspected.
[[nodiscard]] bool isConnected() const;
void disconnectFromHost();
// Releases the socket to the event loop and nulls this handle. B7's
// two-step: the one call site does this from inside the socket's own
// disconnected() handler, where deleting it outright would be a crash.
void deleteLater();
// --- the parked request buffer -------------------------------------------
// Storage lives on the socket object, not in this handle, and must: the
// handle is a value that gets copied into lambdas, while the buffer has to
// survive between two readyRead deliveries on the same connection.
[[nodiscard]] ICoreByteArray buffer() const;
void setBuffer(const ICoreByteArray& data);
void clearBuffer();
// --- the connect seam ----------------------------------------------------
// For connect() and disconnect() ONLY -- readyRead and disconnected.
// --- the connect seam's replacement ---------------------------------------
//
// ⚠ ATTACHED TO THE UNDERLYING SOCKET, NOT TO THIS WRAPPER. ICoreTcpSocket
// is a VALUE that call sites copy into lambdas, so a hook stored per-copy
// would fire on whichever copy happened to register it. Both of these
// register against the socket itself, so every copy observes the same
// stream -- which is what the five call sites this replaces relied on
// QObject::connect for.
void onReadyRead(std::function<void()> fn);
void onDisconnected(std::function<void()> fn);
// ⚠ qt() STOOD HERE AND IS GONE (A9.10, 2026-08-22). It was "B7's connect
// seam and nothing more" -- readyRead and disconnected -- and the two hooks
// directly above are those two signals, registered against the socket itself
// so every copy of this value handle observes the same stream. With them
// landed the accessor had no caller in the tree, and an accessor with no
// caller is only a Qt name in a portable header.
// Hidden friends: declared here so ADL finds them, DEFINED in the .cpp
// because a body in a header is a body wherever it sits.
friend bool operator==(const ICoreTcpSocket& a, const ICoreTcpSocket& b) noexcept;
friend bool operator!=(const ICoreTcpSocket& a, const ICoreTcpSocket& b) noexcept;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreUdpSocket.h#
ICoreEssentials/Network/ICoreUdpSocket.h
ICoreUdpSocket -- datagrams: bind, send, receive, broadcast, multicast.
ICoreUdpSocket socket; socket.bind(ICoreHostAddress::any(ICoreHostAddress::Family::IPv4), 45454); socket.setReadyReadHandler([&] { while (auto d = socket.readDatagram()) handle(d->data, d->sender, d->senderPort); }); socket.writeDatagram(bytes, *ICoreHostAddress::parse("192.0.2.7"), 45454);
Reads never block: readDatagram() returns std::nullopt when nothing is waiting. The ready-read handler runs on the MAIN thread, through the application's event loop (an ICoreApplication must be running), once per batch of arrivals -- drain everything waiting each time it runs.
ICoreUdpDatagram#
ICoreUdpSocket.h:42 · struct · 0 declaration(s)
Opened by row PS5.18 of the Platform SDK product plan.
struct ICoreUdpDatagram {
public:
ICoreByteArray data;
ICoreHostAddress sender;
std::uint16_t senderPort = 0;
};
};
ICoreUdpSocket#
ICoreUdpSocket.h:48 · class · pImpl · 17 declaration(s)
class ICoreUdpSocket {
public:
ICoreUdpSocket();
~ICoreUdpSocket();
ICoreUdpSocket(const ICoreUdpSocket&) = delete;
ICoreUdpSocket& operator=(const ICoreUdpSocket&) = delete;
// Port 0 lets the OS pick one; localPort() then says which.
bool bind(const ICoreHostAddress& address, std::uint16_t port);
[[nodiscard]] bool isBound() const noexcept;
[[nodiscard]] ICoreHostAddress localAddress() const;
[[nodiscard]] std::uint16_t localPort() const noexcept;
// The number of bytes sent, or -1 (see errorString()).
std::int64_t writeDatagram(const ICoreByteArray& data, const ICoreHostAddress& to, std::uint16_t port);
[[nodiscard]] bool hasPendingDatagrams() const;
[[nodiscard]] std::optional<ICoreUdpDatagram> readDatagram();
void setReadyReadHandler(std::function<void()> handler);
bool setBroadcast(bool enabled);
bool joinMulticastGroup(const ICoreHostAddress& group);
bool leaveMulticastGroup(const ICoreHostAddress& group);
void close();
[[nodiscard]] std::string errorString() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreUrl.h#
ICoreEssentials/Network/ICoreUrl.h
ICoreUrl -- the address of a thing to fetch.
QT-FREE (phase 3). The member is the URL's text and this header names no Qt type; <QUrl> moves to ICoreHttpClient.h, which is where the QNetworkRequest that actually needs a parsed URL lives. It leaves the umbrella's prerequisite block either way.
B13 refused an
operator QUrland gave this wrapper only a NAMED toQUrl(), which is what made the swap possible: nothing outside the layer ever held the QUrl, so no call site could notice it leaving. toQUrl() had exactly one reader -- ICoreHttpRequest::setUrl -- and it now takes the text and parses it itself, one line away from the QNetworkRequest it was always feeding.⚠ WHAT THE CALL SITES ACTUALLY USE, measured before writing any of this, is
ICoreUrl#
ICoreUrl.h:66 · class · pImpl · 11 declaration(s)
class ICoreUrl {
public:
ICoreUrl();
~ICoreUrl();
// Explicit, as B13 made it: an implicit ICoreString -> ICoreUrl conversion
// would put this type into overload sets that take strings.
explicit ICoreUrl(const ICoreString& url);
// Copyable, as it always was -- a URL is a value. Written out because a
// unique_ptr<Impl> member deletes the implicit copy.
ICoreUrl(const ICoreUrl& other);
ICoreUrl& operator=(const ICoreUrl& other);
// Decodes %XX escapes and '+' is NOT treated as a space, matching
// QUrl::fromPercentEncoding. The runner's form parser splits on '&' and '='
// first and hands each half here, so a literal '+' in a value stays a '+'
// exactly as it did before.
//
// MALFORMED INPUT DIVERGES FROM Qt, deliberately, and this is the one
// place in the row where the two disagree on purpose.
//
// * A TRUNCATED tail -- "%", "%A", "x%", "%2" -- is copied through
// verbatim. This IS Qt's behaviour; verified.
// * A three-character escape with NON-HEX digits is also copied through
// verbatim here. Qt does something else: its decoder runs the hex
// conversion anyway and emits whatever byte falls out, so "%GG" decodes
// to 'w' and "%ZZ" to U+FFFD. That is undocumented garbage-in,
// garbage-out, not a contract, and reproducing it would mean copying an
// internal quirk on purpose.
//
// Safe for the one caller: the runner's query parser reads percent-encoding
// a browser generated, which is always well formed. On input that is not,
// the old code produced mojibake and this produces the text as typed.
[[nodiscard]] static ICoreString fromPercentEncoding(const ICoreByteArray& encoded);
// See the header note: this is a shape check, not QUrl::isValid.
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool isEmpty() const;
[[nodiscard]] ICoreString toString() const;
[[nodiscard]] ICoreString scheme() const;
// The authority's host, with any userinfo and port stripped. IPv6 literals
// keep their brackets, as QUrl::host does not -- one of several places this
// is a simplification rather than a reimplementation. See the header note.
[[nodiscard]] ICoreString host() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreWebSocket.h#
ICoreEssentials/Network/ICoreWebSocket.h
ICoreWebSocket -- an RFC 6455 client: messages both ways over one long-lived connection.
ICoreWebSocket ws; ws.setOpenedHandler([&] { ws.sendText("{\"subscribe\":\"prices\"}"); }); ws.setTextMessageHandler([&](const std::string& text) { handle(text); }); ws.setClosedHandler([&](int code, const std::string& reason) { ... }); ws.open("wss://example.com/feed");
It rides on each platform's own stack, so TLS, certificates, proxies and HTTP/1.1 upgrade are the operating system's: NSURLSessionWebSocketTask on macOS and iPadOS, WinHTTP on Windows, libcurl on Linux (a libcurl built with WebSocket support -- 8.11 and later enable it by default -- or open() fails and says so), and the browser's WebSocket on the web. Android has no seat
ICoreWebSocket#
ICoreWebSocket.h:48 · class · pImpl · 19 declaration(s)
Opened by row PS5.19 of the Platform SDK product plan.
class ICoreWebSocket {
public:
enum class State : int {
Closed = 0,
Connecting = 1,
Open = 2,
Closing = 3,
};
ICoreWebSocket();
// An open connection is closed with 1001 ("going away"); no handler runs
// after the destructor starts.
~ICoreWebSocket();
ICoreWebSocket(const ICoreWebSocket&) = delete;
ICoreWebSocket& operator=(const ICoreWebSocket&) = delete;
// Starts connecting to a ws:// or wss:// URL, offering `protocols` as
// subprotocols. False, with errorString() set and no handler called, when
// the URL is not a WebSocket URL, the socket is not Closed, or this
// platform has no seat.
bool open(const std::string& url, const std::vector<std::string>& protocols = {});
[[nodiscard]] State state() const noexcept;
[[nodiscard]] std::string url() const;
// The subprotocol the server chose; empty until open, or if it chose none.
[[nodiscard]] std::string protocol() const;
// False unless the connection is Open (and, for text, valid UTF-8).
bool sendText(const std::string& utf8);
bool sendBinary(const ICoreByteArray& data);
// Starts the closing handshake; the closed handler follows. False for a
// code or reason the protocol does not allow, or when not Open.
bool close(int code = 1000, const std::string& reason = std::string());
// After the closed handler. When THIS side called close(), the code and
// reason it sent -- the peer's reply is not visible on every platform
// (NSURLSession keeps it to itself), so no platform reports it. When the
// PEER closed first, the peer's code and reason. After a failure, 1006,
// an empty reason, and errorString() says what went wrong.
[[nodiscard]] int closeCode() const noexcept;
[[nodiscard]] std::string closeReason() const;
[[nodiscard]] std::string errorString() const;
void setOpenedHandler(std::function<void()> handler);
void setTextMessageHandler(std::function<void(const std::string& text)> handler);
void setBinaryMessageHandler(std::function<void(const ICoreByteArray& data)> handler);
void setErrorHandler(std::function<void(const std::string& error)> handler);
void setClosedHandler(std::function<void(int code, const std::string& reason)> handler);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreWebSocketSeat.h#
ICoreEssentials/Network/Backends/ICoreWebSocketSeat.h
INTERNAL -- the seam between ICoreWebSocket (Network/ICoreWebSocket.cpp, portable) and the platform stack that carries the connection. Not installed (sdk_surface.txt: every path under a Backends/ directory is internal).
One seat per platform, each chosen by name in ICorePlatformLibrary.cmake:
Native/Apple/ICoreWebSocket.mm NSURLSessionWebSocketTask (macOS, iPadOS) Native/Linux/ICoreWebSocket.cpp libcurl's WebSocket API (curl_ws_*) WinHttp/ICoreWebSocket.cpp WinHttpWebSocket* Native/Web/ICoreWebSocket.cpp the browser's own WebSocket; on Android the same file answers "not available"
A seat reports through the events below, from ANY thread, one at a time and in order, and never after its destructor has returned. It does not hop to
ICoreWebSocketSeatEvents#
ICoreWebSocketSeat.h:27 · struct · 3 declaration(s)
struct ICoreWebSocketSeatEvents {
public:
// The handshake succeeded; `protocol` is the subprotocol the server chose,
// or empty.
std::function<void(const std::string& protocol)> opened;
// One whole message, reassembled from its fragments.
std::function<void(bool binary, std::string&& payload)> message;
// The connection is over, and nothing follows. A clean close carries the
// peer's code and reason and an empty `error`; a failure carries 1006 and
// says what went wrong in `error`. Reported exactly once.
std::function<void(int code, const std::string& reason, const std::string& error)> closed;
};
};
ICoreWebSocketSeat#
ICoreWebSocketSeat.h:41 · class · 3 declaration(s)
class ICoreWebSocketSeat {
public:
virtual ~ICoreWebSocketSeat();
// Queued in order; never blocks. Called only between opened and closed.
virtual void send(bool binary, const std::string& payload) = 0;
// Starts the closing handshake; `closed` follows when it completes (or
// fails). Called at most once.
virtual void close(int code, const std::string& reason) = 0;
};
};
ICoreNativeSocketApi.h#
ICoreEssentials/Network/Backends/Native/ICoreNativeSocketApi.h
The ONE place the native socket seat spells the platform -- W9.18, 2026-08-28, and it is
Text/Backends/Native/ICoreIcu.h's argument in a second tier.⚠⚠ SOCKETS ARE NOT SPELLED THE SAME WAY ON EVERY PLATFORM, AND UNTIL THIS HEADER THE "NATIVE" SEAT ASSUMED THEY WERE.
ICoreTcpSocket.cppopened with <poll.h>, <sys/socket.h>, <unistd.h> andICoreTcpServer.cppadded <arpa/inet.h> and <netinet/in.h> -- so<Tier>/Backends/Native/meant "not Qt" rather than "portable", which is exactly what W9.14 split the Text, Process and Container tiers to stop meaning and never reached this one. Measured on MSVC 19.44.35228 ARM64, Windows SDK 10.0.26100.0:ICoreTcpSocket.cpp(21): fatal error C1083: Cannot open include file: 'poll.h': No such file or directory ICoreTcpServer.cpp(8): fatal error C1083: Cannot open include file:
Declares no class of its own — see the file.
ICoreNativeSocketCore.h#
ICoreEssentials/Network/Backends/Native/ICoreNativeSocketCore.h
One accepted connection on the native socket seat: the descriptor, the reader thread that drains it, and the two hooks a caller registers.
⚠ THE OWNERSHIP MODEL IS COPIED FROM THE QT SEAT DELIBERATELY, because the call sites depend on it. ICoreTcpSocket is a NON-OWNING HANDLE whose copies alias -- the pre-release runner captures copies into lambdas and expects them all to see one stream -- and the server owns every connection it accepted, so destroying the server takes them with it. Anything else compiles and then behaves differently.
⚠ AND THE PARKED REQUEST BUFFER LIVES ON THE CONNECTION, NOT ON THE HANDLE, for the same reason the Qt seat keeps it as a dynamic property on the socket: it must survive between two readyRead deliveries on one connection, while handles are copied and discarded freely.
Connection#
ICoreNativeSocketCore.h:35 · class · pImpl · 13 declaration(s)
class Connection {
public:
explicit Connection(int fd);
~Connection();
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
[[nodiscard]] int fd() const noexcept;
[[nodiscard]] bool connected() const noexcept;
// Both hooks are delivered through the Process tier's dispatcher, so they
// arrive on the thread that owns the loop rather than on the reader thread.
void onReadyRead(std::function<void()> fn);
void onDisconnected(std::function<void()> fn);
[[nodiscard]] std::string takeIncoming();
long long write(const char* data, std::size_t n);
void disconnect(); // half-close: the peer sees EOF
void close();
[[nodiscard]] std::string takeBuffer() const; // the parked request buffer
void setBuffer(const std::string& data);
void startReader(const std::shared_ptr<Connection>& self);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};