Files
nym/wasm/smolmix/internal-dev/mix-socket.js
T
mfahampshire 43a1bd38e8 Max/smolmix wasm (#6784)
* Mod gitignore + license trimming + comment trimming

* Big rewrite

* SURB inputs + DNS button in internal-dev

* Make ipr addr optional

* Accidentatly omitted files from rewrite commit

* Makefile + readme

* Comment rewrite

* Optimisation comment

* Replace manual waker map with
      smoltcp built-ins + adaptive poll

* Comments

* Extract socket creation helpers into stream.rs

* Cleanup comments

* Comment

* Comment notes and restrict ciphersuites wrt rustls-rustcrypto

* Dep. hack fix for demo + add clearnet fetch() for contrast

* Stripped down devtester

* Fix Clippy arg (fatfingered deletion)

* CodeRabbit catches

* Cargofmt

* Review nits: bridge logs, fetch early-return, static port counter, copyright years, README + Cargo + headless.js tidying

* PHONY + taskset override, switch internal-dev/tests to pnpm, fix wasm-pack out-dir

* Gate codec tests behind the codec feature for no-default-features builds

* IPv6 addr/route on smoltcp iface + configurable DNS resolvers via TunnelOpts

* DNS GUI inputs, close stale WS on reconnect, worker init guards + ws-send warning, Playwright listener cleanup, pnpm-lock in internal-dev

* Fix lp -> lp-data after rebase

* Revert nym-lp/nym-lp-data feature-gating left over from rebase

* Lift getrandom wasm_js cfg to workspace .cargo/config.toml so cargo check -p smolmix-wasm works from any CWD

* temp will amend git message

* Auto-discover IPR when none specified + 'Use random IPR' checkbox in internal-dev

* smolmix_tracker + State machine + ready_tunnel gate + getTunnelState JS surface

* Mirror red display() entries to console.error

* Add left out package-lock

* Reactor clock + yield_now + atomic seq + gateway-storage errors

* setupMixTunnel gate + MTU 1980 + http::Uri cleanup

* Review pass + fix test + clippy

* restore axum 0.8 bump from borked earlier merge

* Feature gating (dns/fetch/socket) + TunnelOptsBuilder + pnpm bypass

* Cont. with review comments

* tokio Nofity reactor wakes + cancellation + setup polishing

* Notify wakes + inner pattern + close_notify + util

* Tunable tunnelopts

* Fix tired commit

* CI prep

* Lint + Clippy

* coderabbit u32 fix

* nits + runtime debugging + expose in internal-dev

* remove redudant default-features

* Remove more redundant default-features
2026-05-28 15:57:10 +00:00

231 lines
5.9 KiB
JavaScript

// MixSocket — drop-in WebSocket replacement over the Nym mixnet.
//
// Mirrors the standard browser WebSocket API (RFC 6455):
//
// const ws = new MixSocket('wss://echo.example.com/ws');
// ws.onopen = () => ws.send('hello');
// ws.onmessage = (e) => console.log(e.data);
// ws.onclose = (e) => console.log(e.code, e.reason);
//
// Communicates with the worker via raw postMessage (not Comlink).
// The worker maps connId → WASM handleId and forwards events back.
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
let _worker = null;
let _nextConnId = 1;
const _instances = new Map();
function _onWorkerMessage(event) {
const msg = event.data;
if (msg?.kind !== 'ws-event') return;
const instance = _instances.get(msg.connId);
if (!instance) return;
instance._handleEvent(msg.type, msg.data);
}
export class MixSocket extends EventTarget {
static CONNECTING = CONNECTING;
static OPEN = OPEN;
static CLOSING = CLOSING;
static CLOSED = CLOSED;
/**
* Bind the raw Worker so MixSocket can post messages to it.
* Call once during app setup, after the worker emits 'Loaded'.
*/
static _initWorker(worker) {
_worker = worker;
_worker.addEventListener('message', _onWorkerMessage);
}
/**
* @param {string} url - WebSocket URL (ws:// or wss://)
* @param {string|string[]} [protocols] - Sub-protocol(s) to negotiate
*/
constructor(url, protocols) {
super();
if (!_worker) {
throw new Error(
'MixSocket: worker not initialised — call MixSocket._initWorker(worker) first',
);
}
this._connId = _nextConnId++;
this._url = url;
this._readyState = CONNECTING;
this._protocol = '';
this._binaryType = 'blob';
// Standard event handler properties
this.onopen = null;
this.onmessage = null;
this.onclose = null;
this.onerror = null;
_instances.set(this._connId, this);
const protoList = protocols
? typeof protocols === 'string'
? [protocols]
: [...protocols]
: [];
_worker.postMessage({
kind: 'ws-connect',
connId: this._connId,
url,
protocols: protoList,
});
}
get url() {
return this._url;
}
get readyState() {
return this._readyState;
}
get protocol() {
return this._protocol;
}
get extensions() {
return '';
}
get binaryType() {
return this._binaryType;
}
set binaryType(val) {
if (val === 'blob' || val === 'arraybuffer') this._binaryType = val;
}
get bufferedAmount() {
return 0;
}
/**
* Send data over the WebSocket.
* @param {string|ArrayBuffer|ArrayBufferView} data
*/
send(data) {
if (this._readyState !== OPEN) {
throw new DOMException('WebSocket is not open', 'InvalidStateError');
}
// Normalise typed arrays to Uint8Array for structured clone
let payload = data;
if (data instanceof ArrayBuffer) {
payload = new Uint8Array(data);
} else if (ArrayBuffer.isView(data) && !(data instanceof Uint8Array)) {
payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
_worker.postMessage({
kind: 'ws-send',
connId: this._connId,
payload,
});
}
/**
* Initiate the closing handshake.
* @param {number} [code=1000] - Status code
* @param {string} [reason=''] - Human-readable reason
*/
close(code = 1000, reason = '') {
if (this._readyState === CLOSING || this._readyState === CLOSED) return;
this._readyState = CLOSING;
_worker.postMessage({
kind: 'ws-close',
connId: this._connId,
code,
reason,
});
}
/** @internal Route an event from the worker to the appropriate handler. */
_handleEvent(type, data) {
switch (type) {
case 'open': {
this._readyState = OPEN;
this._protocol = data || '';
const ev = new Event('open');
this.dispatchEvent(ev);
if (this.onopen) this.onopen(ev);
break;
}
case 'text': {
const ev = new MessageEvent('message', { data });
this.dispatchEvent(ev);
if (this.onmessage) this.onmessage(ev);
break;
}
case 'binary': {
let payload;
if (this._binaryType === 'arraybuffer') {
payload = data instanceof Uint8Array ? data.buffer : data;
} else {
// Default: blob
payload = data instanceof Uint8Array ? new Blob([data]) : data;
}
const ev = new MessageEvent('message', { data: payload });
this.dispatchEvent(ev);
if (this.onmessage) this.onmessage(ev);
break;
}
case 'close': {
this._readyState = CLOSED;
_instances.delete(this._connId);
// Parse close info: "1000 normal closure" → code=1000, reason="normal closure"
let code = 1005;
let reason = '';
if (typeof data === 'string') {
const match = data.match(/^(\d+)\s*(.*)/);
if (match) {
code = parseInt(match[1], 10);
reason = match[2] || '';
} else {
reason = data;
}
}
const ev = new CloseEvent('close', {
code,
reason,
wasClean: code === 1000,
});
this.dispatchEvent(ev);
if (this.onclose) this.onclose(ev);
break;
}
case 'error': {
this._readyState = CLOSED;
_instances.delete(this._connId);
const errorEv = new Event('error');
this.dispatchEvent(errorEv);
if (this.onerror) this.onerror(errorEv);
// Spec: error is always followed by close (code 1006 = abnormal closure)
const closeEv = new CloseEvent('close', {
code: 1006,
reason: typeof data === 'string' ? data : '',
wasClean: false,
});
this.dispatchEvent(closeEv);
if (this.onclose) this.onclose(closeEv);
break;
}
}
}
}