- --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>
322 lines
13 KiB
JavaScript
322 lines
13 KiB
JavaScript
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// JS bridge for the CRAFTER_NETWORK_BROWSER build of Crafter.Network.
|
|
// Populates `window.crafter_webbuild_env` with the env imports the .wasm
|
|
// declares (crafterNetworkFetch, crafterNetworkWt*), and drives the
|
|
// reader loops that feed responses back into the wasm exports
|
|
// (CrafterNetworkOnFetchComplete, CrafterNetworkOnWt*).
|
|
//
|
|
// Crafter.Build's wasi-browser runtime merges this object into the
|
|
// WebAssembly import object as the `env` module before instantiation —
|
|
// mirrors additional/dom-env.js in Crafter.Graphics.
|
|
|
|
const __decoder = new TextDecoder();
|
|
const __encoder = new TextEncoder();
|
|
|
|
function __wasm() { return window.crafter_webbuild_wasi.instance.exports; }
|
|
function __memBuf() { return window.crafter_webbuild_wasi.instance.exports.memory.buffer; }
|
|
|
|
function __readUtf8(ptr, len) {
|
|
return __decoder.decode(new Uint8Array(__memBuf(), ptr, len));
|
|
}
|
|
function __readBytes(ptr, len) {
|
|
// Copy out of the linear memory — the underlying buffer is detached
|
|
// whenever wasm grows its memory, so handing the view straight back
|
|
// to fetch / WebTransport would risk operating on a freed view.
|
|
return new Uint8Array(__memBuf(), ptr, len).slice();
|
|
}
|
|
function __allocCopy(bytes) {
|
|
const ptr = __wasm().WasmAlloc(bytes.length);
|
|
new Uint8Array(__memBuf(), ptr, bytes.length).set(bytes);
|
|
return ptr;
|
|
}
|
|
function __allocUtf8(str) {
|
|
return __allocCopy(__encoder.encode(str));
|
|
}
|
|
|
|
// ─── fetch() bridge ───────────────────────────────────────────────────
|
|
|
|
function crafterNetworkFetch(methodPtr, methodLen,
|
|
urlPtr, urlLen,
|
|
headersPtr, headersLen,
|
|
bodyPtr, bodyLen,
|
|
callbackId) {
|
|
const method = __readUtf8(methodPtr, methodLen);
|
|
const url = __readUtf8(urlPtr, urlLen);
|
|
const init = { method, headers: {} };
|
|
|
|
if (headersLen > 0) {
|
|
const headerStr = __readUtf8(headersPtr, headersLen);
|
|
for (const line of headerStr.split('\n')) {
|
|
const sep = line.indexOf(': ');
|
|
if (sep > 0) init.headers[line.slice(0, sep)] = line.slice(sep + 2);
|
|
}
|
|
}
|
|
if (bodyLen > 0 && method !== 'GET' && method !== 'HEAD') {
|
|
init.body = __readBytes(bodyPtr, bodyLen);
|
|
}
|
|
|
|
fetch(url, init).then(async (response) => {
|
|
// Lowercase to match HTTP/3 convention the native client uses.
|
|
const headerLines = [];
|
|
response.headers.forEach((value, name) => headerLines.push(`${name.toLowerCase()}: ${value}`));
|
|
const headerStr = headerLines.join('\n');
|
|
const bodyBuf = await response.arrayBuffer();
|
|
|
|
const headerEncoded = __encoder.encode(headerStr);
|
|
const headerPtr = headerEncoded.length > 0 ? __allocCopy(headerEncoded) : 0;
|
|
const respBodyPtr = bodyBuf.byteLength > 0 ? __allocCopy(new Uint8Array(bodyBuf)) : 0;
|
|
__wasm().CrafterNetworkOnFetchComplete(callbackId,
|
|
response.status,
|
|
headerPtr, headerEncoded.length,
|
|
respBodyPtr, bodyBuf.byteLength);
|
|
}).catch((err) => {
|
|
const msg = String(err && err.message ? err.message : err);
|
|
const encoded = __encoder.encode(msg);
|
|
const ptr = encoded.length > 0 ? __allocCopy(encoded) : 0;
|
|
__wasm().CrafterNetworkOnFetchError(callbackId, ptr, encoded.length);
|
|
});
|
|
}
|
|
|
|
// ─── WebTransport bridge ──────────────────────────────────────────────
|
|
//
|
|
// Connection-handle and stream-handle counters are monotone. Each
|
|
// connection's incoming-stream + datagram reader loops are started as
|
|
// soon as wt.ready resolves and run until the session closes. Outgoing
|
|
// operations queued before ready are gated behind a per-handle `ready`
|
|
// promise.
|
|
|
|
let __wtNextHandle = 0;
|
|
let __wtNextStream = 0;
|
|
const __wtSessions = new Map(); // handle → { wt, ready, streams: Set<streamId> }
|
|
const __wtStreams = new Map(); // streamId → { connection, writer, reader, writable, readable }
|
|
|
|
function crafterNetworkWtConnect(hostPtr, hostLen, port, alpnPtr, alpnLen, certHashPtr, certHashLen) {
|
|
const host = __readUtf8(hostPtr, hostLen);
|
|
const alpn = __readUtf8(alpnPtr, alpnLen);
|
|
const url = `https://${host}:${port}/${alpn}`;
|
|
const opts = {};
|
|
if (certHashLen > 0) {
|
|
const hash = __readBytes(certHashPtr, certHashLen);
|
|
opts.serverCertificateHashes = [{ algorithm: 'sha-256', value: hash }];
|
|
}
|
|
|
|
let wt;
|
|
try {
|
|
wt = new WebTransport(url, opts);
|
|
} catch (err) {
|
|
console.error('Crafter.Network: WebTransport ctor failed:', err);
|
|
return 0;
|
|
}
|
|
|
|
const handle = ++__wtNextHandle;
|
|
const session = { wt, ready: false, streams: new Set(), closed: false };
|
|
__wtSessions.set(handle, session);
|
|
|
|
wt.ready.then(() => {
|
|
session.ready = true;
|
|
__wasm().CrafterNetworkOnWtReady(handle);
|
|
__wtRunIncomingLoop(handle, wt.incomingBidirectionalStreams, /*bidi=*/true);
|
|
__wtRunIncomingLoop(handle, wt.incomingUnidirectionalStreams, /*bidi=*/false);
|
|
__wtRunDatagramLoop(handle, wt);
|
|
}).catch((err) => {
|
|
__wtFireClosed(handle, String(err && err.message ? err.message : err));
|
|
});
|
|
|
|
wt.closed.then(() => {
|
|
__wtFireClosed(handle, '');
|
|
}).catch((err) => {
|
|
__wtFireClosed(handle, String(err && err.message ? err.message : err));
|
|
});
|
|
|
|
return handle;
|
|
}
|
|
|
|
function __wtFireClosed(handle, message) {
|
|
const session = __wtSessions.get(handle);
|
|
if (!session || session.closed) return;
|
|
session.closed = true;
|
|
// Wake up any per-stream receivers with a synthetic FIN so the C++
|
|
// state machine terminates pending callbacks instead of hanging.
|
|
for (const streamId of session.streams) {
|
|
__wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1);
|
|
__wtStreams.delete(streamId);
|
|
}
|
|
session.streams.clear();
|
|
const msgPtr = message ? __allocUtf8(message) : 0;
|
|
__wasm().CrafterNetworkOnWtClosed(handle, msgPtr, message ? __encoder.encode(message).length : 0);
|
|
}
|
|
|
|
function crafterNetworkWtClose(handle) {
|
|
const session = __wtSessions.get(handle);
|
|
if (!session) return;
|
|
try { session.wt.close(); } catch (_) { /* already closing */ }
|
|
__wtSessions.delete(handle);
|
|
}
|
|
|
|
function crafterNetworkWtOpenStream(handle, unidirectional) {
|
|
const session = __wtSessions.get(handle);
|
|
if (!session || session.closed) return 0;
|
|
const streamId = ++__wtNextStream;
|
|
const record = { connection: handle, writer: null, reader: null,
|
|
pendingOps: [], opened: false, unidirectional: !!unidirectional };
|
|
__wtStreams.set(streamId, record);
|
|
session.streams.add(streamId);
|
|
|
|
const opener = session.ready
|
|
? Promise.resolve()
|
|
: session.wt.ready;
|
|
opener.then(() => {
|
|
const p = unidirectional
|
|
? session.wt.createUnidirectionalStream()
|
|
: session.wt.createBidirectionalStream();
|
|
return p;
|
|
}).then((stream) => {
|
|
// For a bidi stream `stream` is a WebTransportBidirectionalStream
|
|
// with .readable and .writable. For a unidi outgoing stream
|
|
// `stream` is itself a WritableStream.
|
|
if (unidirectional) {
|
|
record.writer = stream.getWriter();
|
|
} else {
|
|
record.writer = stream.writable.getWriter();
|
|
record.reader = stream.readable.getReader();
|
|
__wtRunStreamReader(streamId);
|
|
}
|
|
record.opened = true;
|
|
for (const op of record.pendingOps) op();
|
|
record.pendingOps.length = 0;
|
|
}).catch((err) => {
|
|
console.error(`Crafter.Network: openStream(${streamId}) failed`, err);
|
|
// Tell C++ the stream is dead so pending receivers unblock.
|
|
__wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1);
|
|
__wtStreams.delete(streamId);
|
|
session.streams.delete(streamId);
|
|
});
|
|
|
|
return streamId;
|
|
}
|
|
|
|
function crafterNetworkWtStreamWrite(streamId, bufPtr, bufLen, finish, callbackId) {
|
|
const record = __wtStreams.get(streamId);
|
|
// The completion's second argument says whether the stream took the write.
|
|
if (!record) {
|
|
if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId, 0);
|
|
return;
|
|
}
|
|
const data = bufLen > 0 ? __readBytes(bufPtr, bufLen) : new Uint8Array(0);
|
|
const doWrite = () => {
|
|
const p = data.length > 0 ? record.writer.write(data) : Promise.resolve();
|
|
p.then(() => {
|
|
if (finish) return record.writer.close();
|
|
}).then(() => {
|
|
if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId, 1);
|
|
}).catch((err) => {
|
|
console.error(`Crafter.Network: write(${streamId}) failed`, err);
|
|
if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId, 0);
|
|
});
|
|
};
|
|
if (record.opened) doWrite();
|
|
else record.pendingOps.push(doWrite);
|
|
}
|
|
|
|
function crafterNetworkWtStreamStop(streamId) {
|
|
const record = __wtStreams.get(streamId);
|
|
if (!record) return;
|
|
try { record.writer && record.writer.close(); } catch (_) {}
|
|
try { record.reader && record.reader.cancel(); } catch (_) {}
|
|
const session = __wtSessions.get(record.connection);
|
|
if (session) session.streams.delete(streamId);
|
|
__wtStreams.delete(streamId);
|
|
}
|
|
|
|
function crafterNetworkWtSendDatagram(handle, bufPtr, bufLen) {
|
|
const session = __wtSessions.get(handle);
|
|
if (!session || session.closed) return;
|
|
const data = __readBytes(bufPtr, bufLen);
|
|
const send = () => {
|
|
const writer = session.wt.datagrams.writable.getWriter();
|
|
writer.write(data).catch((err) => {
|
|
console.error('Crafter.Network: sendDatagram failed', err);
|
|
}).finally(() => {
|
|
try { writer.releaseLock(); } catch (_) {}
|
|
});
|
|
};
|
|
if (session.ready) send();
|
|
else session.wt.ready.then(send).catch(() => {});
|
|
}
|
|
|
|
async function __wtRunStreamReader(streamId) {
|
|
const record = __wtStreams.get(streamId);
|
|
if (!record || !record.reader) return;
|
|
try {
|
|
while (true) {
|
|
const { value, done } = await record.reader.read();
|
|
if (value && value.byteLength > 0) {
|
|
const ptr = __allocCopy(value);
|
|
__wasm().CrafterNetworkOnWtStreamChunk(streamId, ptr, value.byteLength, done ? 1 : 0);
|
|
} else if (done) {
|
|
__wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1);
|
|
}
|
|
if (done) break;
|
|
}
|
|
} catch (err) {
|
|
// Reader cancelled or stream closed. Dispatch a synthetic FIN so
|
|
// any pending C++ receiver wakes up.
|
|
__wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1);
|
|
}
|
|
}
|
|
|
|
async function __wtRunIncomingLoop(handle, source, bidi) {
|
|
const session = __wtSessions.get(handle);
|
|
if (!session) return;
|
|
const reader = source.getReader();
|
|
try {
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
const streamId = ++__wtNextStream;
|
|
const record = { connection: handle, writer: null, reader: null,
|
|
pendingOps: [], opened: true, unidirectional: !bidi };
|
|
if (bidi) {
|
|
record.writer = value.writable.getWriter();
|
|
record.reader = value.readable.getReader();
|
|
} else {
|
|
record.reader = value.getReader();
|
|
}
|
|
__wtStreams.set(streamId, record);
|
|
session.streams.add(streamId);
|
|
__wasm().CrafterNetworkOnWtIncomingStream(handle, streamId, bidi ? 1 : 0);
|
|
__wtRunStreamReader(streamId);
|
|
}
|
|
} catch (_) { /* session closed */ }
|
|
}
|
|
|
|
async function __wtRunDatagramLoop(handle, wt) {
|
|
const reader = wt.datagrams.readable.getReader();
|
|
try {
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
if (value && value.byteLength > 0) {
|
|
const ptr = __allocCopy(value);
|
|
__wasm().CrafterNetworkOnWtDatagram(handle, ptr, value.byteLength);
|
|
}
|
|
}
|
|
} catch (_) { /* session closed */ }
|
|
}
|
|
|
|
// ─── Export env object ────────────────────────────────────────────────
|
|
|
|
if (!window.crafter_webbuild_env) {
|
|
window.crafter_webbuild_env = {};
|
|
}
|
|
Object.assign(window.crafter_webbuild_env, {
|
|
crafterNetworkFetch,
|
|
crafterNetworkWtConnect,
|
|
crafterNetworkWtClose,
|
|
crafterNetworkWtOpenStream,
|
|
crafterNetworkWtStreamWrite,
|
|
crafterNetworkWtStreamStop,
|
|
crafterNetworkWtSendDatagram,
|
|
});
|