43a1bd38e8
* 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
231 lines
5.9 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|
|
}
|