329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
/*
|
|
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,
|
|
});
|