browser wasm
This commit is contained in:
parent
28fab2509b
commit
e8630528af
24 changed files with 2490 additions and 100 deletions
329
additional/network-env.js
Normal file
329
additional/network-env.js
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
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,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue