Crafter.Network/implementations/Crafter.Network-ClientQUIC-Browser.cpp

427 lines
17 KiB
C++
Raw Normal View History

2026-07-22 17:55:45 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®ation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
2026-05-19 02:53:50 +02:00
// CRAFTER_NETWORK_BROWSER implementation of ClientQUIC / QUICStream backed
// by the browser's WebTransport API. WebTransport is HTTP/3-based and
// expects a URL — we use https://${host}:${port}/${alpn}. Native msquic
// is not linked into the browser build.
//
// Async-only: synchronous send/receive methods on QUICStream are not
// compiled (gated out in the interface). Everything goes through the
// existing *Async / OnStream / OnDatagram callbacks. The JS bridge
// (additional/network-env.js) runs the WebTransport reader loops and
// dispatches each chunk back through the wasm exports declared below.
module;
module Crafter.Network:ClientQUIC_impl;
import :ClientQUIC;
import std;
using namespace Crafter;
namespace Crafter::NetworkBrowserBindings {
__attribute__((import_module("env"), import_name("crafterNetworkWtConnect")))
std::int32_t crafterNetworkWtConnect(
const char* host, std::int32_t hostLen,
std::int32_t port,
const char* alpn, std::int32_t alpnLen,
const std::uint8_t* certHash, std::int32_t certHashLen);
__attribute__((import_module("env"), import_name("crafterNetworkWtClose")))
void crafterNetworkWtClose(std::int32_t handle);
__attribute__((import_module("env"), import_name("crafterNetworkWtOpenStream")))
std::int32_t crafterNetworkWtOpenStream(std::int32_t handle, std::int32_t unidirectional);
__attribute__((import_module("env"), import_name("crafterNetworkWtStreamWrite")))
void crafterNetworkWtStreamWrite(std::int32_t streamId,
const char* buf, std::int32_t bufLen,
std::int32_t finish,
std::int32_t callbackId);
__attribute__((import_module("env"), import_name("crafterNetworkWtStreamStop")))
void crafterNetworkWtStreamStop(std::int32_t streamId);
__attribute__((import_module("env"), import_name("crafterNetworkWtSendDatagram")))
void crafterNetworkWtSendDatagram(std::int32_t handle,
const char* buf, std::int32_t bufLen);
}
namespace Crafter::NetworkBrowser {
// ─── Receive state machine per stream ─────────────────────────────────
//
// The JS reader loop pushes every chunk it sees through
// CrafterNetworkOnWtStreamChunk. We buffer them until the user calls
// one of the Receive*Async variants — at which point we either
// dispatch immediately (chunks already queued) or wait for the next
// chunk to arrive. A FIN signal marks the end of the peer's send-
// side; further chunks after FIN do not arrive.
//
// StreamState / ConnectionState live in a named namespace (not
// anonymous) because they are referenced from the QUICStream::Impl
// and ClientQUIC::Impl definitions below — private nested types of
// exported classes can't have TU-local member types without a
// diagnostic.
enum class RecvMode { None, Once, UntilClose, UntilFull };
struct StreamState {
// 0 means destroyed / closed.
std::int32_t handle = 0;
// Buffered chunks not yet delivered to a pending callback.
std::vector<char> buffer;
bool finReceived = false;
bool closed = false;
// Pending one-shot receive.
RecvMode mode = RecvMode::None;
std::uint32_t target = 0;
std::function<void(std::vector<char>)> cb;
ClientQUIC* connection = nullptr;
};
struct ConnectionState {
std::int32_t handle = 0;
bool ready = false;
bool closed = false;
std::function<void(QUICStream)> onStream;
std::function<void(std::vector<char>)> onDatagram;
};
// Handle → state. Allocated on the heap so the pointer is stable
// across QUICStream / ClientQUIC moves (each holds a unique_ptr<Impl>
// that wraps a pointer into these maps via its handle).
inline std::unordered_map<std::int32_t, StreamState*>& Streams() {
static std::unordered_map<std::int32_t, StreamState*> m;
return m;
}
inline std::unordered_map<std::int32_t, ConnectionState*>& Connections() {
static std::unordered_map<std::int32_t, ConnectionState*> m;
return m;
}
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived - --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with them msquic: a consumer that only needs TCP builds no msquic. The flag isn't passed on to dependencies. - --local takes Crafter.Thread from the sibling checkout, as the other Crafter projects do. - ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and SocketTimeoutException. - TCP receives return the bytes that arrived: - RecieveSync(n) returned the whole buffer when fewer had come; - RecieveUntilCloseSync truncated what it had read; - a signal (EINTR) during a receive threw where it should wait on; - RecieveSync(size, buffer) returns the count and throws like the others; - a receive of no bytes returns at once: recv's 0 for it isn't a close. - Every TCP *Async receive takes an onError(std::exception_ptr): an exception escaping a ThreadPool task ended the program. An empty onError drops the error. - QUIC streams: - async receives deliver an empty chunk on close, as the browser build does; - RecieveUntilFullAsync gives what arrived on a stream that closes early; - RecieveUntilFullSync takes all it asked for or nothing, under one hold of the lock, so what did arrive stays for the next receive and no other receive can take bytes from among it; - SendAsync's callback hears whether the send went out (bool sent): native, browser, the JS bridge and the example. - msquic is pinned to v2.6.3 and built against the system OpenSSL (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with config_diagnostics on) made it fail to initialise. The README says to use --no-quic where the distro's OpenSSL is older. - Tests now register on any Linux triple: they never ran under the Flatpak's x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP, ShouldRecieveTCP, ShouldReportClosedQUICStream. Breaking: SendAsync's callback takes a bool, the TCP *Async receives take an onError, and RecieveSync(size, buffer) returns the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
inline std::unordered_map<std::int32_t, std::function<void(bool)>>& WriteCallbacks() {
static std::unordered_map<std::int32_t, std::function<void(bool)>> m;
2026-05-19 02:53:50 +02:00
return m;
}
inline std::int32_t NextWriteCallbackId() {
static std::int32_t counter = 0;
return ++counter;
}
inline void TryDispatchRecv(StreamState& s) {
if (s.mode == RecvMode::None) return;
if (s.mode == RecvMode::Once) {
if (!s.buffer.empty()) {
auto chunk = std::move(s.buffer);
s.buffer.clear();
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
} else if (s.finReceived || s.closed) {
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb({});
}
return;
}
if (s.mode == RecvMode::UntilClose) {
if (s.finReceived || s.closed) {
auto chunk = std::move(s.buffer);
s.buffer.clear();
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
}
return;
}
if (s.mode == RecvMode::UntilFull) {
if (s.buffer.size() >= s.target) {
std::vector<char> chunk(s.buffer.begin(), s.buffer.begin() + s.target);
s.buffer.erase(s.buffer.begin(), s.buffer.begin() + s.target);
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
} else if (s.finReceived || s.closed) {
// Peer closed before we got the requested byte count —
// deliver whatever's left. Mirrors the native variant's
// "throws if peer closes early" only loosely (we have no
// exception channel from JS).
auto chunk = std::move(s.buffer);
s.buffer.clear();
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
}
}
}
}
// All implementation-private state types live in Crafter::NetworkBrowser
// (above). Pull them in unqualified for readability.
using Crafter::NetworkBrowser::StreamState;
using Crafter::NetworkBrowser::ConnectionState;
using Crafter::NetworkBrowser::RecvMode;
using Crafter::NetworkBrowser::Streams;
using Crafter::NetworkBrowser::Connections;
using Crafter::NetworkBrowser::WriteCallbacks;
using Crafter::NetworkBrowser::NextWriteCallbackId;
using Crafter::NetworkBrowser::TryDispatchRecv;
// ─── QUICStream::Impl ────────────────────────────────────────────────────
struct QUICStream::Impl {
StreamState state;
};
QUICStream::QUICStream() : impl(std::make_unique<Impl>()) {}
QUICStream::QUICStream(std::int32_t streamHandle, ClientQUIC* conn,
bool canSendArg, bool canReceiveArg)
: canSend(canSendArg), canReceive(canReceiveArg),
impl(std::make_unique<Impl>())
{
connection = conn;
impl->state.handle = streamHandle;
impl->state.connection = conn;
Streams()[streamHandle] = &impl->state;
}
QUICStream::QUICStream(QUICStream&&) noexcept = default;
QUICStream& QUICStream::operator=(QUICStream&&) noexcept = default;
QUICStream::~QUICStream() {
if (impl && impl->state.handle != 0) {
Streams().erase(impl->state.handle);
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle);
}
}
void QUICStream::SendAsync(const void* buffer, std::uint32_t size, bool finish,
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived - --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with them msquic: a consumer that only needs TCP builds no msquic. The flag isn't passed on to dependencies. - --local takes Crafter.Thread from the sibling checkout, as the other Crafter projects do. - ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and SocketTimeoutException. - TCP receives return the bytes that arrived: - RecieveSync(n) returned the whole buffer when fewer had come; - RecieveUntilCloseSync truncated what it had read; - a signal (EINTR) during a receive threw where it should wait on; - RecieveSync(size, buffer) returns the count and throws like the others; - a receive of no bytes returns at once: recv's 0 for it isn't a close. - Every TCP *Async receive takes an onError(std::exception_ptr): an exception escaping a ThreadPool task ended the program. An empty onError drops the error. - QUIC streams: - async receives deliver an empty chunk on close, as the browser build does; - RecieveUntilFullAsync gives what arrived on a stream that closes early; - RecieveUntilFullSync takes all it asked for or nothing, under one hold of the lock, so what did arrive stays for the next receive and no other receive can take bytes from among it; - SendAsync's callback hears whether the send went out (bool sent): native, browser, the JS bridge and the example. - msquic is pinned to v2.6.3 and built against the system OpenSSL (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with config_diagnostics on) made it fail to initialise. The README says to use --no-quic where the distro's OpenSSL is older. - Tests now register on any Linux triple: they never ran under the Flatpak's x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP, ShouldRecieveTCP, ShouldReportClosedQUICStream. Breaking: SendAsync's callback takes a bool, the TCP *Async receives take an onError, and RecieveSync(size, buffer) returns the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
std::function<void(bool sent)> onSent) {
2026-05-19 02:53:50 +02:00
if (!impl || impl->state.handle == 0 || impl->state.closed) {
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived - --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with them msquic: a consumer that only needs TCP builds no msquic. The flag isn't passed on to dependencies. - --local takes Crafter.Thread from the sibling checkout, as the other Crafter projects do. - ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and SocketTimeoutException. - TCP receives return the bytes that arrived: - RecieveSync(n) returned the whole buffer when fewer had come; - RecieveUntilCloseSync truncated what it had read; - a signal (EINTR) during a receive threw where it should wait on; - RecieveSync(size, buffer) returns the count and throws like the others; - a receive of no bytes returns at once: recv's 0 for it isn't a close. - Every TCP *Async receive takes an onError(std::exception_ptr): an exception escaping a ThreadPool task ended the program. An empty onError drops the error. - QUIC streams: - async receives deliver an empty chunk on close, as the browser build does; - RecieveUntilFullAsync gives what arrived on a stream that closes early; - RecieveUntilFullSync takes all it asked for or nothing, under one hold of the lock, so what did arrive stays for the next receive and no other receive can take bytes from among it; - SendAsync's callback hears whether the send went out (bool sent): native, browser, the JS bridge and the example. - msquic is pinned to v2.6.3 and built against the system OpenSSL (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with config_diagnostics on) made it fail to initialise. The README says to use --no-quic where the distro's OpenSSL is older. - Tests now register on any Linux triple: they never ran under the Flatpak's x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP, ShouldRecieveTCP, ShouldReportClosedQUICStream. Breaking: SendAsync's callback takes a bool, the TCP *Async receives take an onError, and RecieveSync(size, buffer) returns the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
if (onSent) onSent(false);
2026-05-19 02:53:50 +02:00
return;
}
std::int32_t cbId = 0;
if (onSent) {
cbId = NextWriteCallbackId();
WriteCallbacks()[cbId] = std::move(onSent);
}
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamWrite(
impl->state.handle,
static_cast<const char*>(buffer),
static_cast<std::int32_t>(size),
finish ? 1 : 0,
cbId);
}
void QUICStream::RecieveAsync(std::function<void(std::vector<char>)> cb) {
if (!impl) { if (cb) cb({}); return; }
impl->state.mode = RecvMode::Once;
impl->state.cb = std::move(cb);
TryDispatchRecv(impl->state);
}
void QUICStream::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> cb) {
if (!impl) { if (cb) cb({}); return; }
impl->state.mode = RecvMode::UntilClose;
impl->state.cb = std::move(cb);
TryDispatchRecv(impl->state);
}
void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize,
std::function<void(std::vector<char>)> cb) {
if (!impl) { if (cb) cb({}); return; }
impl->state.mode = RecvMode::UntilFull;
impl->state.target = bufferSize;
impl->state.cb = std::move(cb);
TryDispatchRecv(impl->state);
}
void QUICStream::Stop() {
if (impl && impl->state.handle != 0) {
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle);
Streams().erase(impl->state.handle);
impl->state.handle = 0;
impl->state.closed = true;
}
}
// ─── ClientQUIC::Impl ────────────────────────────────────────────────────
struct ClientQUIC::Impl {
ConnectionState state;
};
namespace {
QUICStream MakeStreamFromHandle(std::int32_t streamId, ClientQUIC* conn,
bool canSend, bool canReceive) {
return QUICStream{streamId, conn, canSend, canReceive};
}
}
ClientQUIC::ClientQUIC(const char* host, std::uint16_t port, std::string alpnArg,
QUICClientCredentials creds)
: alpn(std::move(alpnArg)), impl(std::make_unique<Impl>()) {
// Zeroed hash means "no pinning" — JS passes an empty array, browser
// falls back to its trust store. A non-zero hash is forwarded as a
// serverCertificateHashes entry (Chrome only, < 14 day cert validity).
const std::uint8_t* hashPtr = nullptr;
std::int32_t hashLen = 0;
for (std::uint8_t b : creds.serverCertificateHash) {
if (b != 0) { hashPtr = creds.serverCertificateHash.data(); hashLen = 32; break; }
}
std::string hostStr = host;
impl->state.handle = Crafter::NetworkBrowserBindings::crafterNetworkWtConnect(
hostStr.data(), static_cast<std::int32_t>(hostStr.size()),
static_cast<std::int32_t>(port),
alpn.data(), static_cast<std::int32_t>(alpn.size()),
hashPtr, hashLen);
// wasm builds run with -fno-exceptions; a failed JS-side allocation
// leaves the connection in a closed state and the first operation will
// produce a sentinel result (OpenStream returns a default-constructed
// QUICStream, SendDatagram silently drops, OnStream/OnDatagram never
// fire). The constructor cannot signal the failure synchronously.
if (impl->state.handle == 0) {
impl->state.closed = true;
} else {
Connections()[impl->state.handle] = &impl->state;
}
}
ClientQUIC::ClientQUIC(std::string host, std::uint16_t port, std::string alpnArg,
QUICClientCredentials creds)
: ClientQUIC(host.c_str(), port, std::move(alpnArg), std::move(creds)) {}
ClientQUIC::ClientQUIC(ClientQUIC&&) noexcept = default;
ClientQUIC::~ClientQUIC() {
if (impl && impl->state.handle != 0) {
Connections().erase(impl->state.handle);
Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle);
}
}
QUICStream ClientQUIC::OpenStream(bool unidirectional) {
if (!impl || impl->state.handle == 0 || impl->state.closed) {
return QUICStream{}; // default-constructed: closed sentinel
}
std::int32_t streamId = Crafter::NetworkBrowserBindings::crafterNetworkWtOpenStream(
impl->state.handle, unidirectional ? 1 : 0);
if (streamId == 0) {
return QUICStream{};
}
return MakeStreamFromHandle(streamId, this,
/*canSend=*/true,
/*canReceive=*/!unidirectional);
}
void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) {
if (!impl || impl->state.handle == 0 || impl->state.closed) return;
Crafter::NetworkBrowserBindings::crafterNetworkWtSendDatagram(
impl->state.handle,
static_cast<const char*>(buffer),
static_cast<std::int32_t>(size));
}
void ClientQUIC::OnStream(std::function<void(QUICStream)> cb) {
if (impl) impl->state.onStream = std::move(cb);
}
void ClientQUIC::OnDatagram(std::function<void(std::vector<char>)> cb) {
if (impl) impl->state.onDatagram = std::move(cb);
}
void ClientQUIC::Stop() {
if (impl && impl->state.handle != 0) {
Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle);
Connections().erase(impl->state.handle);
impl->state.handle = 0;
impl->state.closed = true;
}
}
// ─── WASM exports the JS bridge dispatches back through ──────────────────
extern "C" {
__attribute__((export_name("CrafterNetworkOnWtReady")))
void CrafterNetworkOnWtReady(std::int32_t connectionHandle) {
auto it = Connections().find(connectionHandle);
if (it == Connections().end()) return;
it->second->ready = true;
}
__attribute__((export_name("CrafterNetworkOnWtClosed")))
void CrafterNetworkOnWtClosed(std::int32_t connectionHandle,
char* messagePtr, std::int32_t /*messageLen*/) {
std::free(messagePtr);
auto it = Connections().find(connectionHandle);
if (it == Connections().end()) return;
it->second->closed = true;
it->second->ready = false;
// The JS bridge dispatches CrafterNetworkOnWtStreamChunk(_, nullptr, 0,
// fin=1) for each stream that belonged to this connection so pending
// receivers terminate. We don't iterate stream state here.
}
__attribute__((export_name("CrafterNetworkOnWtIncomingStream")))
void CrafterNetworkOnWtIncomingStream(std::int32_t connectionHandle,
std::int32_t streamId,
std::int32_t bidirectional) {
auto it = Connections().find(connectionHandle);
if (it == Connections().end() || !it->second->onStream) {
// No registered handler — close the stream JS-side to free
// resources. Mirrors what the native msquic backend does
// when a stream arrives before OnStream is registered (it
// queues, but in the browser we don't have a backing buffer
// to queue against without leaking).
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(streamId);
return;
}
QUICStream stream = MakeStreamFromHandle(streamId, /*conn=*/nullptr,
/*canSend=*/bidirectional != 0,
/*canReceive=*/true);
it->second->onStream(std::move(stream));
}
__attribute__((export_name("CrafterNetworkOnWtStreamChunk")))
void CrafterNetworkOnWtStreamChunk(std::int32_t streamId,
char* dataPtr, std::int32_t dataLen,
std::int32_t fin) {
auto it = Streams().find(streamId);
if (it == Streams().end()) { std::free(dataPtr); return; }
StreamState& s = *it->second;
if (dataPtr && dataLen > 0) {
s.buffer.insert(s.buffer.end(), dataPtr, dataPtr + dataLen);
}
std::free(dataPtr);
if (fin) s.finReceived = true;
TryDispatchRecv(s);
}
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived - --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with them msquic: a consumer that only needs TCP builds no msquic. The flag isn't passed on to dependencies. - --local takes Crafter.Thread from the sibling checkout, as the other Crafter projects do. - ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and SocketTimeoutException. - TCP receives return the bytes that arrived: - RecieveSync(n) returned the whole buffer when fewer had come; - RecieveUntilCloseSync truncated what it had read; - a signal (EINTR) during a receive threw where it should wait on; - RecieveSync(size, buffer) returns the count and throws like the others; - a receive of no bytes returns at once: recv's 0 for it isn't a close. - Every TCP *Async receive takes an onError(std::exception_ptr): an exception escaping a ThreadPool task ended the program. An empty onError drops the error. - QUIC streams: - async receives deliver an empty chunk on close, as the browser build does; - RecieveUntilFullAsync gives what arrived on a stream that closes early; - RecieveUntilFullSync takes all it asked for or nothing, under one hold of the lock, so what did arrive stays for the next receive and no other receive can take bytes from among it; - SendAsync's callback hears whether the send went out (bool sent): native, browser, the JS bridge and the example. - msquic is pinned to v2.6.3 and built against the system OpenSSL (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with config_diagnostics on) made it fail to initialise. The README says to use --no-quic where the distro's OpenSSL is older. - Tests now register on any Linux triple: they never ran under the Flatpak's x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP, ShouldRecieveTCP, ShouldReportClosedQUICStream. Breaking: SendAsync's callback takes a bool, the TCP *Async receives take an onError, and RecieveSync(size, buffer) returns the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
// `sent` is nonzero when the write (and the close, with finish) resolved.
2026-05-19 02:53:50 +02:00
__attribute__((export_name("CrafterNetworkOnWtStreamWriteComplete")))
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived - --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with them msquic: a consumer that only needs TCP builds no msquic. The flag isn't passed on to dependencies. - --local takes Crafter.Thread from the sibling checkout, as the other Crafter projects do. - ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and SocketTimeoutException. - TCP receives return the bytes that arrived: - RecieveSync(n) returned the whole buffer when fewer had come; - RecieveUntilCloseSync truncated what it had read; - a signal (EINTR) during a receive threw where it should wait on; - RecieveSync(size, buffer) returns the count and throws like the others; - a receive of no bytes returns at once: recv's 0 for it isn't a close. - Every TCP *Async receive takes an onError(std::exception_ptr): an exception escaping a ThreadPool task ended the program. An empty onError drops the error. - QUIC streams: - async receives deliver an empty chunk on close, as the browser build does; - RecieveUntilFullAsync gives what arrived on a stream that closes early; - RecieveUntilFullSync takes all it asked for or nothing, under one hold of the lock, so what did arrive stays for the next receive and no other receive can take bytes from among it; - SendAsync's callback hears whether the send went out (bool sent): native, browser, the JS bridge and the example. - msquic is pinned to v2.6.3 and built against the system OpenSSL (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with config_diagnostics on) made it fail to initialise. The README says to use --no-quic where the distro's OpenSSL is older. - Tests now register on any Linux triple: they never ran under the Flatpak's x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP, ShouldRecieveTCP, ShouldReportClosedQUICStream. Breaking: SendAsync's callback takes a bool, the TCP *Async receives take an onError, and RecieveSync(size, buffer) returns the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
void CrafterNetworkOnWtStreamWriteComplete(std::int32_t callbackId, std::int32_t sent) {
2026-05-19 02:53:50 +02:00
auto it = WriteCallbacks().find(callbackId);
if (it == WriteCallbacks().end()) return;
auto cb = std::move(it->second);
WriteCallbacks().erase(it);
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived - --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with them msquic: a consumer that only needs TCP builds no msquic. The flag isn't passed on to dependencies. - --local takes Crafter.Thread from the sibling checkout, as the other Crafter projects do. - ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and SocketTimeoutException. - TCP receives return the bytes that arrived: - RecieveSync(n) returned the whole buffer when fewer had come; - RecieveUntilCloseSync truncated what it had read; - a signal (EINTR) during a receive threw where it should wait on; - RecieveSync(size, buffer) returns the count and throws like the others; - a receive of no bytes returns at once: recv's 0 for it isn't a close. - Every TCP *Async receive takes an onError(std::exception_ptr): an exception escaping a ThreadPool task ended the program. An empty onError drops the error. - QUIC streams: - async receives deliver an empty chunk on close, as the browser build does; - RecieveUntilFullAsync gives what arrived on a stream that closes early; - RecieveUntilFullSync takes all it asked for or nothing, under one hold of the lock, so what did arrive stays for the next receive and no other receive can take bytes from among it; - SendAsync's callback hears whether the send went out (bool sent): native, browser, the JS bridge and the example. - msquic is pinned to v2.6.3 and built against the system OpenSSL (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with config_diagnostics on) made it fail to initialise. The README says to use --no-quic where the distro's OpenSSL is older. - Tests now register on any Linux triple: they never ran under the Flatpak's x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP, ShouldRecieveTCP, ShouldReportClosedQUICStream. Breaking: SendAsync's callback takes a bool, the TCP *Async receives take an onError, and RecieveSync(size, buffer) returns the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
if (cb) cb(sent != 0);
2026-05-19 02:53:50 +02:00
}
__attribute__((export_name("CrafterNetworkOnWtDatagram")))
void CrafterNetworkOnWtDatagram(std::int32_t connectionHandle,
char* dataPtr, std::int32_t dataLen) {
auto it = Connections().find(connectionHandle);
if (it == Connections().end() || !it->second->onDatagram) {
std::free(dataPtr);
return;
}
std::vector<char> data(dataPtr, dataPtr + dataLen);
std::free(dataPtr);
it->second->onDatagram(std::move(data));
}
}