//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Network:Stream; import std; #ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { // A reliable, ordered byte stream with deadlines on both directions. // // This exists so the HTTP/1.1 client and listener can be written once and // run over either a bare socket or a TLS session: `PlainStream` below is // the `http://` transport, `TLSStream` (in :TLS) the `https://` one. The // HTTP/1.1 code holds a `ByteStream&` and never learns which it has. // // Every method takes its own timeout rather than the stream carrying one, // because HTTP/1.1 uses different budgets for different states — a long // idle keep-alive wait, a shorter one once a request has started. export enum class StreamStatus { Data, // `read` bytes are available in the buffer Closed, // the peer closed its send side, cleanly TimedOut, // nothing arrived before the deadline }; export class ByteStream { public: virtual ~ByteStream() = default; ByteStream() = default; ByteStream(const ByteStream&) = delete; ByteStream& operator=(const ByteStream&) = delete; // Read whatever is already available, waiting at most `timeout` for // the first byte. Sets `read` and returns Data, or reports a clean // close / a timeout. Throws on a transport error. virtual StreamStatus ReadSome(char* buffer, std::size_t size, std::chrono::milliseconds timeout, std::size_t& read) = 0; // Write the whole buffer. Throws if it could not all be handed over // within `timeout`. virtual void Write(const void* buffer, std::size_t size, std::chrono::milliseconds timeout) = 0; // Best-effort orderly close of our send side. Never throws — it runs // on teardown paths where there is nothing useful to do with a // failure. virtual void Shutdown() noexcept = 0; // The underlying descriptor, so a listener can shutdown(2) it to wake // a thread parked in poll(). virtual int Descriptor() const noexcept = 0; // Negotiated ALPN protocol, empty when the transport has no notion of // one (plaintext) or nothing was agreed. virtual std::string_view Protocol() const noexcept { return {}; } // Whether the bytes are encrypted on the wire. virtual bool Secure() const noexcept { return false; } }; // Plaintext TCP. Non-owning: the descriptor stays owned by the ClientTCP // (or whatever else) that opened it. // // The descriptor is switched to non-blocking on construction — both // directions are driven by poll() against a deadline, which a blocking // descriptor cannot express. That is also what lets a write time out // instead of parking forever against a peer that has stopped reading. export class PlainStream final : public ByteStream { public: explicit PlainStream(int descriptor); StreamStatus ReadSome(char* buffer, std::size_t size, std::chrono::milliseconds timeout, std::size_t& read) override; void Write(const void* buffer, std::size_t size, std::chrono::milliseconds timeout) override; void Shutdown() noexcept override; int Descriptor() const noexcept override { return descriptor; } private: int descriptor; }; // Put a descriptor into non-blocking mode. Exposed because :TLS needs the // same thing for the descriptor it wraps. export void SetNonBlocking(int descriptor); // Wait until `descriptor` is ready for `events` (a poll(2) event mask) or // `deadline` passes; false means the deadline won. Retries across EINTR // and throws on a real poll failure. Shared with :TLS, which has to poll // for whichever direction OpenSSL asks for next. export bool PollDescriptor(int descriptor, short events, std::chrono::steady_clock::time_point deadline); } #endif