Crafter.Network/additional/network-env.js

329 lines
13 KiB
JavaScript
Raw Permalink Normal View History

2026-05-19 02:53:50 +02:00
/*
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<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);
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,
});