wasmnet 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # wasmnet
2
2
 
3
- Browser client for [wasmnet](https://github.com/anistark/wasmnet) — a networking proxy that bridges WASI socket APIs to real TCP via WebSocket.
3
+ Browser client for [wasmnet](https://github.com/anistark/wasmnet) — a networking proxy that bridges WASI socket APIs to real TCP/UDP/TLS via WebSocket.
4
4
 
5
5
  ## Install
6
6
 
@@ -16,28 +16,28 @@ import { WasmnetClient } from 'wasmnet';
16
16
  const client = new WasmnetClient('ws://localhost:9000');
17
17
  await client.ready();
18
18
 
19
- // Outbound TCP connection
20
- const id = await client.connect('api.example.com', 443);
19
+ // TCP
20
+ const id = await client.connect('example.com', 80);
21
+ client.onData(id, (data) => console.log(new TextDecoder().decode(data)));
22
+ client.send(id, 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
21
23
 
22
- client.onData(id, (data) => {
23
- console.log('received:', new TextDecoder().decode(data));
24
- });
24
+ // TLS (server handles handshake)
25
+ const tls = await client.connectTls('api.example.com', 443);
26
+ client.onData(tls, (data) => console.log('tls:', data));
25
27
 
26
- client.onClose(id, () => {
27
- console.log('connection closed');
28
- });
28
+ // UDP
29
+ const udp = await client.connectUdp('8.8.8.8', 53);
30
+ client.onDataFrom(udp.id, (data, addr, port) => console.log(data));
31
+ client.send(udp.id, packet);
32
+ client.sendTo(udp.id, '8.8.4.4', 53, packet); // different target
29
33
 
30
- client.send(id, 'GET / HTTP/1.1\r\nHost: api.example.com\r\n\r\n');
34
+ // DNS resolve
35
+ const ips = await client.resolve('example.com');
31
36
 
32
- // Inbound TCP (bind a port)
37
+ // Inbound TCP
33
38
  const listener = await client.bind('0.0.0.0', 3000);
34
- console.log(`listening on port ${listener.port}`);
35
-
36
39
  client.onAccept(listener.id, (connId, remote) => {
37
- console.log(`accepted connection from ${remote}`);
38
- client.onData(connId, (data) => {
39
- client.send(connId, data); // echo
40
- });
40
+ client.onData(connId, (data) => client.send(connId, data));
41
41
  });
42
42
 
43
43
  // Cleanup
@@ -45,20 +45,36 @@ client.close(id);
45
45
  client.disconnect();
46
46
  ```
47
47
 
48
+ ### Binary framing
49
+
50
+ Pass `{ binary: true }` to skip JSON + base64 overhead on data frames. Raw bytes are sent directly in WebSocket binary messages.
51
+
52
+ ```javascript
53
+ const client = new WasmnetClient('ws://localhost:9000', { binary: true });
54
+ ```
55
+
48
56
  ## API
49
57
 
50
- ### `new WasmnetClient(url: string)`
58
+ ### `new WasmnetClient(url: string, options?: { binary?: boolean })`
51
59
 
52
- Creates a client connecting to the wasmnet server at the given WebSocket URL.
60
+ Creates a client connecting to the wasmnet server. Set `binary: true` for binary framing mode.
53
61
 
54
62
  ### `ready(): Promise<void>`
55
63
 
56
- Resolves when the WebSocket connection is established.
64
+ Resolves when the WebSocket connection is open.
57
65
 
58
66
  ### `connect(addr: string, port: number): Promise<number>`
59
67
 
60
68
  Opens an outbound TCP connection. Returns the socket ID.
61
69
 
70
+ ### `connectTls(addr: string, port: number): Promise<number>`
71
+
72
+ Opens an outbound TCP connection with TLS. The wasmnet server handles the TLS handshake (using system CA roots). Returns the socket ID. Data sent/received through this socket is plaintext — encryption is handled transparently.
73
+
74
+ ### `connectUdp(addr: string, port: number): Promise<{ id: number, port: number }>`
75
+
76
+ Creates a UDP socket connected to the given address. Returns the socket ID and local port. Use `send()` to send to the connected address, or `sendTo()` for arbitrary targets.
77
+
62
78
  ### `bind(addr: string, port: number): Promise<{ id: number, port: number }>`
63
79
 
64
80
  Binds a TCP listener. Returns the listener ID and actual bound port.
@@ -69,7 +85,15 @@ Starts accepting connections on a bound listener.
69
85
 
70
86
  ### `send(id: number, data: string | Uint8Array | ArrayBuffer): void`
71
87
 
72
- Sends data on a socket. Strings are UTF-8 encoded, binary data is sent as-is.
88
+ Sends data on a TCP, TLS, or connected UDP socket.
89
+
90
+ ### `sendTo(id: number, addr: string, port: number, data: string | Uint8Array | ArrayBuffer): void`
91
+
92
+ Sends a UDP datagram to a specific target address, regardless of the socket's connected address.
93
+
94
+ ### `resolve(name: string): Promise<string[]>`
95
+
96
+ Resolves a hostname to an array of IP address strings (both IPv4 and IPv6).
73
97
 
74
98
  ### `close(id: number): void`
75
99
 
@@ -77,7 +101,11 @@ Closes a socket or listener.
77
101
 
78
102
  ### `onData(id: number, callback: (data: Uint8Array) => void): void`
79
103
 
80
- Registers a data handler. Any buffered data is flushed immediately.
104
+ Registers a data handler for TCP/TLS sockets. Any data buffered before the handler was set is flushed immediately.
105
+
106
+ ### `onDataFrom(id: number, callback: (data: Uint8Array, addr: string, port: number) => void): void`
107
+
108
+ Registers a data handler for UDP sockets. Each callback includes the source address and port.
81
109
 
82
110
  ### `onClose(id: number, callback: () => void): void`
83
111
 
@@ -85,10 +113,12 @@ Registers a close handler.
85
113
 
86
114
  ### `onAccept(id: number, callback: (connId: number, remote: string) => void): void`
87
115
 
88
- Registers an accept handler for a listener.
116
+ Registers an accept handler for a TCP listener. `connId` is the new socket ID for the accepted connection.
89
117
 
90
118
  ### `disconnect(): void`
91
119
 
92
120
  Closes the WebSocket connection and all sockets.
93
121
 
94
- ## MIT License
122
+ ## License
123
+
124
+ MIT
@@ -3,20 +3,56 @@ export interface ListenResult {
3
3
  port: number;
4
4
  }
5
5
 
6
+ export interface ClientOptions {
7
+ /** Use binary framing instead of JSON (lower overhead for data). */
8
+ binary?: boolean;
9
+ }
10
+
6
11
  export type DataCallback = (data: Uint8Array) => void;
12
+ export type DataFromCallback = (
13
+ data: Uint8Array,
14
+ addr: string,
15
+ port: number,
16
+ ) => void;
7
17
  export type CloseCallback = () => void;
8
18
  export type AcceptCallback = (connId: number, remote: string) => void;
9
19
 
10
20
  export class WasmnetClient {
11
- constructor(url: string);
21
+ constructor(url: string, options?: ClientOptions);
12
22
 
13
23
  ready(): Promise<void>;
24
+
25
+ /** TCP connect to a remote host. */
14
26
  connect(addr: string, port: number): Promise<number>;
27
+
28
+ /** TCP connect with TLS (server handles TLS handshake). */
29
+ connectTls(addr: string, port: number): Promise<number>;
30
+
31
+ /** Create a UDP socket connected to a remote host. */
32
+ connectUdp(addr: string, port: number): Promise<ListenResult>;
33
+
34
+ /** Bind a TCP listener. */
15
35
  bind(addr: string, port: number): Promise<ListenResult>;
36
+
16
37
  listen(id: number, backlog?: number): void;
38
+
39
+ /** Send data on a TCP or connected-UDP socket. */
17
40
  send(id: number, data: string | Uint8Array | ArrayBuffer): void;
41
+
42
+ /** Send a UDP datagram to a specific address. */
43
+ sendTo(
44
+ id: number,
45
+ addr: string,
46
+ port: number,
47
+ data: string | Uint8Array | ArrayBuffer,
48
+ ): void;
49
+
50
+ /** Resolve a hostname to IP addresses. */
51
+ resolve(name: string): Promise<string[]>;
52
+
18
53
  close(id: number): void;
19
54
  onData(id: number, callback: DataCallback): void;
55
+ onDataFrom(id: number, callback: DataFromCallback): void;
20
56
  onClose(id: number, callback: CloseCallback): void;
21
57
  onAccept(id: number, callback: AcceptCallback): void;
22
58
  disconnect(): void;
@@ -1,13 +1,48 @@
1
+ const BINARY_HEADER = 9; // 1 byte type + 8 byte id
2
+
3
+ const MSG = {
4
+ // requests
5
+ CONNECT: 0x01,
6
+ BIND: 0x02,
7
+ LISTEN: 0x03,
8
+ SEND: 0x04,
9
+ CLOSE: 0x05,
10
+ CONNECT_UDP: 0x06,
11
+ SEND_TO: 0x07,
12
+ RESOLVE: 0x08,
13
+ CONNECT_TLS: 0x09,
14
+ // events
15
+ CONNECTED: 0x81,
16
+ DATA: 0x82,
17
+ LISTENING: 0x83,
18
+ ACCEPTED: 0x84,
19
+ CLOSED: 0x85,
20
+ ERROR: 0x86,
21
+ DENIED: 0x87,
22
+ DATA_FROM: 0x88,
23
+ RESOLVED: 0x89,
24
+ UDP_BOUND: 0x8a,
25
+ };
26
+
1
27
  export class WasmnetClient {
2
- constructor(url) {
28
+ constructor(url, options = {}) {
3
29
  this.ws = new WebSocket(url);
30
+ this.ws.binaryType = "arraybuffer";
4
31
  this.sockets = new Map();
5
32
  this.nextId = 1;
33
+ this.binary = options.binary === true;
34
+ this._resolves = new Map();
6
35
  this._ready = new Promise((resolve, reject) => {
7
36
  this.ws.onopen = () => resolve();
8
37
  this.ws.onerror = (e) => reject(e);
9
38
  });
10
- this.ws.onmessage = (e) => this._handleEvent(JSON.parse(e.data));
39
+ this.ws.onmessage = (e) => {
40
+ if (e.data instanceof ArrayBuffer) {
41
+ this._handleBinaryEvent(new Uint8Array(e.data));
42
+ } else {
43
+ this._handleEvent(JSON.parse(e.data));
44
+ }
45
+ };
11
46
  this.ws.onclose = () => this._handleClose();
12
47
  }
13
48
 
@@ -18,8 +53,63 @@ export class WasmnetClient {
18
53
  connect(addr, port) {
19
54
  const id = this.nextId++;
20
55
  return new Promise((resolve, reject) => {
21
- this.sockets.set(id, { resolve, reject, buffer: [], onData: null, onClose: null });
22
- this._send({ op: "connect", id, addr, port });
56
+ this.sockets.set(id, {
57
+ resolve,
58
+ reject,
59
+ buffer: [],
60
+ onData: null,
61
+ onClose: null,
62
+ });
63
+ if (this.binary) {
64
+ this._sendBinary(MSG.CONNECT, id, this._addrPortPayload(addr, port));
65
+ } else {
66
+ this._send({ op: "connect", id, addr, port });
67
+ }
68
+ });
69
+ }
70
+
71
+ connectTls(addr, port) {
72
+ const id = this.nextId++;
73
+ return new Promise((resolve, reject) => {
74
+ this.sockets.set(id, {
75
+ resolve,
76
+ reject,
77
+ buffer: [],
78
+ onData: null,
79
+ onClose: null,
80
+ });
81
+ if (this.binary) {
82
+ this._sendBinary(
83
+ MSG.CONNECT_TLS,
84
+ id,
85
+ this._addrPortPayload(addr, port),
86
+ );
87
+ } else {
88
+ this._send({ op: "connect_tls", id, addr, port });
89
+ }
90
+ });
91
+ }
92
+
93
+ connectUdp(addr, port) {
94
+ const id = this.nextId++;
95
+ return new Promise((resolve, reject) => {
96
+ this.sockets.set(id, {
97
+ resolve,
98
+ reject,
99
+ buffer: [],
100
+ onData: null,
101
+ onDataFrom: null,
102
+ onClose: null,
103
+ });
104
+ if (this.binary) {
105
+ this._sendBinary(
106
+ MSG.CONNECT_UDP,
107
+ id,
108
+ this._addrPortPayload(addr, port),
109
+ );
110
+ } else {
111
+ this._send({ op: "connect_udp", id, addr, port });
112
+ }
23
113
  });
24
114
  }
25
115
 
@@ -27,21 +117,81 @@ export class WasmnetClient {
27
117
  const id = this.nextId++;
28
118
  return new Promise((resolve, reject) => {
29
119
  this.sockets.set(id, { resolve, reject, onAccept: null });
30
- this._send({ op: "bind", id, addr, port });
120
+ if (this.binary) {
121
+ this._sendBinary(MSG.BIND, id, this._addrPortPayload(addr, port));
122
+ } else {
123
+ this._send({ op: "bind", id, addr, port });
124
+ }
31
125
  });
32
126
  }
33
127
 
34
128
  listen(id, backlog = 128) {
35
- this._send({ op: "listen", id, backlog });
129
+ if (this.binary) {
130
+ const p = new Uint8Array(4);
131
+ new DataView(p.buffer).setUint32(0, backlog);
132
+ this._sendBinary(MSG.LISTEN, id, p);
133
+ } else {
134
+ this._send({ op: "listen", id, backlog });
135
+ }
36
136
  }
37
137
 
38
138
  send(id, data) {
39
- const encoded = typeof data === "string" ? btoa(data) : this._encodeBinary(data);
40
- this._send({ op: "send", id, data: encoded });
139
+ if (this.binary) {
140
+ const bytes =
141
+ data instanceof Uint8Array
142
+ ? data
143
+ : typeof data === "string"
144
+ ? new TextEncoder().encode(data)
145
+ : new Uint8Array(data);
146
+ this._sendBinary(MSG.SEND, id, bytes);
147
+ } else {
148
+ const encoded =
149
+ typeof data === "string" ? btoa(data) : this._encodeBinary(data);
150
+ this._send({ op: "send", id, data: encoded });
151
+ }
152
+ }
153
+
154
+ sendTo(id, addr, port, data) {
155
+ if (this.binary) {
156
+ const addrBytes = new TextEncoder().encode(addr);
157
+ const raw =
158
+ data instanceof Uint8Array
159
+ ? data
160
+ : typeof data === "string"
161
+ ? new TextEncoder().encode(data)
162
+ : new Uint8Array(data);
163
+ const payload = new Uint8Array(4 + addrBytes.length + raw.length);
164
+ const dv = new DataView(payload.buffer);
165
+ dv.setUint16(0, port);
166
+ dv.setUint16(2, addrBytes.length);
167
+ payload.set(addrBytes, 4);
168
+ payload.set(raw, 4 + addrBytes.length);
169
+ this._sendBinary(MSG.SEND_TO, id, payload);
170
+ } else {
171
+ const encoded =
172
+ typeof data === "string" ? btoa(data) : this._encodeBinary(data);
173
+ this._send({ op: "send_to", id, addr, port, data: encoded });
174
+ }
175
+ }
176
+
177
+ resolve(name) {
178
+ const id = this.nextId++;
179
+ return new Promise((resolve, reject) => {
180
+ this._resolves.set(id, { resolve, reject });
181
+ if (this.binary) {
182
+ this._sendBinary(MSG.RESOLVE, id, new TextEncoder().encode(name));
183
+ } else {
184
+ this._send({ op: "resolve", id, name });
185
+ }
186
+ });
41
187
  }
42
188
 
43
189
  close(id) {
44
- this._send({ op: "close", id });
190
+ if (this.binary) {
191
+ this._sendBinary(MSG.CLOSE, id, new Uint8Array(0));
192
+ } else {
193
+ this._send({ op: "close", id });
194
+ }
45
195
  this.sockets.delete(id);
46
196
  }
47
197
 
@@ -55,6 +205,11 @@ export class WasmnetClient {
55
205
  sock.buffer = [];
56
206
  }
57
207
 
208
+ onDataFrom(id, callback) {
209
+ const sock = this.sockets.get(id);
210
+ if (sock) sock.onDataFrom = callback;
211
+ }
212
+
58
213
  onClose(id, callback) {
59
214
  const sock = this.sockets.get(id);
60
215
  if (sock) sock.onClose = callback;
@@ -69,6 +224,8 @@ export class WasmnetClient {
69
224
  this.ws.close();
70
225
  }
71
226
 
227
+ // ── JSON transport ───────────────────────────────────
228
+
72
229
  _send(msg) {
73
230
  this.ws.send(JSON.stringify(msg));
74
231
  }
@@ -91,7 +248,168 @@ export class WasmnetClient {
91
248
  return bytes;
92
249
  }
93
250
 
251
+ // ── Binary transport ─────────────────────────────────
252
+
253
+ _sendBinary(type_, id, payload) {
254
+ const frame = new Uint8Array(BINARY_HEADER + payload.length);
255
+ frame[0] = type_;
256
+ const dv = new DataView(frame.buffer);
257
+ dv.setBigUint64(1, BigInt(id));
258
+ frame.set(payload, BINARY_HEADER);
259
+ this.ws.send(frame.buffer);
260
+ }
261
+
262
+ _addrPortPayload(addr, port) {
263
+ const addrBytes = new TextEncoder().encode(addr);
264
+ const payload = new Uint8Array(2 + addrBytes.length);
265
+ new DataView(payload.buffer).setUint16(0, port);
266
+ payload.set(addrBytes, 2);
267
+ return payload;
268
+ }
269
+
270
+ _handleBinaryEvent(data) {
271
+ if (data.length < BINARY_HEADER) return;
272
+ const type_ = data[0];
273
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
274
+ const id = Number(dv.getBigUint64(1));
275
+ const payload = data.subarray(BINARY_HEADER);
276
+
277
+ switch (type_) {
278
+ case MSG.CONNECTED:
279
+ this._dispatch(id, "connected", {});
280
+ break;
281
+ case MSG.DATA:
282
+ this._dispatch(id, "data", { raw: payload });
283
+ break;
284
+ case MSG.LISTENING:
285
+ this._dispatch(id, "listening", {
286
+ port: new DataView(
287
+ payload.buffer,
288
+ payload.byteOffset,
289
+ payload.byteLength,
290
+ ).getUint16(0),
291
+ });
292
+ break;
293
+ case MSG.ACCEPTED: {
294
+ const connId = Number(
295
+ new DataView(
296
+ payload.buffer,
297
+ payload.byteOffset,
298
+ payload.byteLength,
299
+ ).getBigUint64(0),
300
+ );
301
+ const remote = new TextDecoder().decode(payload.subarray(8));
302
+ this._dispatch(id, "accepted", { conn_id: connId, remote });
303
+ break;
304
+ }
305
+ case MSG.CLOSED:
306
+ this._dispatch(id, "closed", {});
307
+ break;
308
+ case MSG.ERROR:
309
+ this._dispatch(id, "error", {
310
+ msg: new TextDecoder().decode(payload),
311
+ });
312
+ break;
313
+ case MSG.DENIED:
314
+ this._dispatch(id, "denied", {
315
+ msg: new TextDecoder().decode(payload),
316
+ });
317
+ break;
318
+ case MSG.DATA_FROM: {
319
+ const pdv = new DataView(
320
+ payload.buffer,
321
+ payload.byteOffset,
322
+ payload.byteLength,
323
+ );
324
+ const port = pdv.getUint16(0);
325
+ const addrLen = pdv.getUint16(2);
326
+ const addr = new TextDecoder().decode(payload.subarray(4, 4 + addrLen));
327
+ const raw = payload.subarray(4 + addrLen);
328
+ this._dispatch(id, "data_from", { addr, port, raw });
329
+ break;
330
+ }
331
+ case MSG.RESOLVED: {
332
+ const addrs = JSON.parse(new TextDecoder().decode(payload));
333
+ this._dispatchResolve(id, addrs);
334
+ break;
335
+ }
336
+ case MSG.UDP_BOUND:
337
+ this._dispatch(id, "udp_bound", {
338
+ port: new DataView(
339
+ payload.buffer,
340
+ payload.byteOffset,
341
+ payload.byteLength,
342
+ ).getUint16(0),
343
+ });
344
+ break;
345
+ }
346
+ }
347
+
348
+ _dispatch(id, ev, extra) {
349
+ const sock = this.sockets.get(id);
350
+ if (!sock && ev !== "resolved") return;
351
+
352
+ switch (ev) {
353
+ case "connected":
354
+ sock.resolve?.(id);
355
+ break;
356
+ case "listening":
357
+ sock.resolve?.({ id, port: extra.port });
358
+ break;
359
+ case "udp_bound":
360
+ sock.resolve?.({ id, port: extra.port });
361
+ break;
362
+ case "data": {
363
+ const decoded = extra.raw || this._decodeBinary(extra.b64);
364
+ if (sock.onData) {
365
+ sock.onData(decoded);
366
+ } else {
367
+ sock.buffer.push(decoded);
368
+ }
369
+ break;
370
+ }
371
+ case "data_from":
372
+ sock.onDataFrom?.(extra.raw, extra.addr, extra.port);
373
+ break;
374
+ case "accepted":
375
+ this.sockets.set(extra.conn_id, {
376
+ buffer: [],
377
+ onData: null,
378
+ onClose: null,
379
+ });
380
+ sock.onAccept?.(extra.conn_id, extra.remote);
381
+ break;
382
+ case "closed":
383
+ sock.onClose?.();
384
+ this.sockets.delete(id);
385
+ break;
386
+ case "error":
387
+ sock.reject?.(new Error(extra.msg));
388
+ this.sockets.delete(id);
389
+ break;
390
+ case "denied":
391
+ sock.reject?.(new Error(extra.msg));
392
+ this.sockets.delete(id);
393
+ break;
394
+ }
395
+ }
396
+
397
+ _dispatchResolve(id, addrs) {
398
+ const entry = this._resolves.get(id);
399
+ if (entry) {
400
+ entry.resolve(addrs);
401
+ this._resolves.delete(id);
402
+ }
403
+ }
404
+
405
+ // ── JSON event handler ───────────────────────────────
406
+
94
407
  _handleEvent(ev) {
408
+ if (ev.ev === "resolved") {
409
+ this._dispatchResolve(ev.id, ev.addrs);
410
+ return;
411
+ }
412
+
95
413
  const sock = this.sockets.get(ev.id);
96
414
  if (!sock) return;
97
415
 
@@ -102,6 +420,9 @@ export class WasmnetClient {
102
420
  case "listening":
103
421
  sock.resolve({ id: ev.id, port: ev.port });
104
422
  break;
423
+ case "udp_bound":
424
+ sock.resolve({ id: ev.id, port: ev.port });
425
+ break;
105
426
  case "data": {
106
427
  const decoded = this._decodeBinary(ev.data);
107
428
  if (sock.onData) {
@@ -111,8 +432,17 @@ export class WasmnetClient {
111
432
  }
112
433
  break;
113
434
  }
435
+ case "data_from": {
436
+ const decoded = this._decodeBinary(ev.data);
437
+ sock.onDataFrom?.(decoded, ev.addr, ev.port);
438
+ break;
439
+ }
114
440
  case "accepted":
115
- this.sockets.set(ev.conn_id, { buffer: [], onData: null, onClose: null });
441
+ this.sockets.set(ev.conn_id, {
442
+ buffer: [],
443
+ onData: null,
444
+ onClose: null,
445
+ });
116
446
  sock.onAccept?.(ev.conn_id, ev.remote);
117
447
  break;
118
448
  case "closed":
@@ -131,9 +461,13 @@ export class WasmnetClient {
131
461
  }
132
462
 
133
463
  _handleClose() {
134
- for (const [id, sock] of this.sockets) {
464
+ for (const [, sock] of this.sockets) {
135
465
  sock.onClose?.();
136
466
  }
137
467
  this.sockets.clear();
468
+ for (const [, entry] of this._resolves) {
469
+ entry.reject?.(new Error("connection closed"));
470
+ }
471
+ this._resolves.clear();
138
472
  }
139
473
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wasmnet",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Browser client for wasmnet — networking proxy for WASM",
5
5
  "type": "module",
6
6
  "main": "dist/wasmnet-client.js",