web: JS bindings step + a minimal browser demo

build-web.sh now generates the browser package with wasm-bindgen
(web/pkg/tethercore.js + tethercore_bg.wasm, --target web) after building the
cdylib. web/index.html is a tiny demo: import the bindings, derive the room from
an account, new WasmEngine(...).start(onMessage)/send — relay-only iroh peer in
the browser. Generated web/pkg/ is git-ignored.

End to end now proven buildable: Rust core → wasm32 → wasm-bindgen JS (exports
WasmEngine + roomForAccount) → importable in a browser. A live in-browser run
needs serving web/ + a reachable rendezvous/relay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 17:56:36 -05:00
parent ad026e8871
commit 1135b7d27b
3 changed files with 87 additions and 4 deletions

View File

@@ -18,7 +18,17 @@ export AR_wasm32_unknown_unknown="$LLVM/bin/llvm-ar"
export RUSTFLAGS="${RUSTFLAGS:-} --cfg getrandom_backend=\"wasm_js\""
rustup target add wasm32-unknown-unknown >/dev/null 2>&1 || true
echo "▸ building tethercore for wasm32 (iroh, relay-only in-browser)…"
cargo build --target wasm32-unknown-unknown --features iroh-transport --lib "$@"
echo "✅ target/wasm32-unknown-unknown/debug/tethercore.wasm"
echo " next: wasm-bindgen bindings + a JS shim for the browser app."
PROFILE="${PROFILE:-release}"
echo "▸ building tethercore for wasm32 ($PROFILE; iroh, relay-only in-browser)…"
cargo build --target wasm32-unknown-unknown --features iroh-transport --lib \
$([ "$PROFILE" = release ] && echo --release) "$@"
WASM="target/wasm32-unknown-unknown/$PROFILE/tethercore.wasm"
if command -v wasm-bindgen >/dev/null; then
echo "▸ generating JS bindings → web/pkg/ (wasm-bindgen)…"
wasm-bindgen "$WASM" --out-dir web/pkg --target web
echo "✅ web/pkg/tethercore.js + tethercore_bg.wasm — import into the browser app."
else
echo "⚠ wasm-bindgen CLI not found — built $WASM but skipped JS bindings."
echo " install: cargo install wasm-bindgen-cli --version <match Cargo.lock>"
fi

1
core/web/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/pkg/

72
core/web/index.html Normal file
View File

@@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>tether — web (wasm)</title>
<style>
body { font: 15px/1.5 system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
input, button, textarea { font: inherit; padding: .4rem .5rem; }
.row { display: flex; gap: .5rem; margin: .5rem 0; }
.row input { flex: 1; }
#feed { width: 100%; height: 12rem; }
#status { color: #666; font-size: 13px; }
.dot { display:inline-block;width:10px;height:10px;border-radius:50%;background:#bbb;vertical-align:middle }
.dot.on { background:#2ecc40 }
</style>
</head>
<body>
<h1>tether <small>· web</small> <span id="dot" class="dot"></span></h1>
<p>The same Rust core as the native apps, compiled to wasm — a relay-only iroh
peer in the browser. Point it at your rendezvous and account.</p>
<div class="row"><input id="server" value="https://tether.pecord.io" placeholder="rendezvous URL" /></div>
<div class="row">
<input id="account" value="you@example.com" placeholder="account" />
<button id="connect">Connect</button>
</div>
<div id="status">not connected</div>
<textarea id="feed" readonly placeholder="inbound clipboards appear here…"></textarea>
<div class="row">
<input id="msg" placeholder="type and Send to the room…" />
<button id="send" disabled>Send</button>
</div>
<script type="module">
import init, { WasmEngine, roomForAccount } from "./pkg/tethercore.js";
const $ = (id) => document.getElementById(id);
let engine = null;
$("connect").onclick = async () => {
await init();
const server = $("server").value.trim();
const account = $("account").value.trim();
const room = roomForAccount(account);
const from = "web-" + Math.random().toString(36).slice(2, 10);
engine = new WasmEngine(server, room, from, account);
engine.start((text) => {
$("feed").value += "← " + text + "\n";
$("feed").scrollTop = $("feed").scrollHeight;
});
$("status").textContent = `connecting… room=${room} from=${from}`;
$("dot").classList.add("on");
$("send").disabled = false;
// surface the iroh id once online
const tick = setInterval(() => {
const id = engine.irohId();
if (id) { $("status").textContent = `online · room=${room} · id=${id.slice(0,16)}`; clearInterval(tick); }
}, 500);
};
$("send").onclick = () => {
const text = $("msg").value;
if (engine && text) { engine.send(text); $("feed").value += "→ " + text + "\n"; $("msg").value = ""; }
};
$("msg").addEventListener("keydown", (e) => { if (e.key === "Enter") $("send").click(); });
</script>
</body>
</html>