/* Crafter®.Network Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3.0 of the License, or (at your option) any later version. */ // 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 } 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); if (!record) { if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId); 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) { try { return record.writer.close(); } catch (_) {} } }).then(() => { if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId); }).catch((err) => { console.error(`Crafter.Network: write(${streamId}) failed`, err); if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId); }); }; 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, });