@bytecodealliance/preview2-shim 0.0.21 → 0.14.1

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.
Files changed (44) hide show
  1. package/README.md +4 -14
  2. package/lib/browser/cli.js +2 -4
  3. package/lib/browser/clocks.js +15 -27
  4. package/lib/browser/filesystem.js +2 -30
  5. package/lib/browser/http.js +1 -3
  6. package/lib/browser/io.js +4 -2
  7. package/lib/common/assert.js +7 -0
  8. package/lib/io/calls.js +64 -0
  9. package/lib/io/worker-http.js +95 -0
  10. package/lib/io/worker-io.js +322 -0
  11. package/lib/io/worker-thread.js +569 -0
  12. package/lib/nodejs/cli.js +45 -59
  13. package/lib/nodejs/clocks.js +13 -27
  14. package/lib/nodejs/filesystem.js +539 -459
  15. package/lib/nodejs/http.js +440 -173
  16. package/lib/nodejs/index.js +4 -1
  17. package/lib/nodejs/io.js +1 -0
  18. package/lib/nodejs/sockets/socket-common.js +116 -0
  19. package/lib/nodejs/sockets/socketopts-bindings.js +94 -0
  20. package/lib/nodejs/sockets/tcp-socket-impl.js +794 -0
  21. package/lib/nodejs/sockets/udp-socket-impl.js +628 -0
  22. package/lib/nodejs/sockets/wasi-sockets.js +320 -0
  23. package/lib/nodejs/sockets.js +11 -200
  24. package/lib/synckit/index.js +4 -2
  25. package/package.json +1 -5
  26. package/types/interfaces/wasi-cli-terminal-input.d.ts +4 -0
  27. package/types/interfaces/wasi-cli-terminal-output.d.ts +4 -0
  28. package/types/interfaces/wasi-clocks-monotonic-clock.d.ts +19 -6
  29. package/types/interfaces/wasi-filesystem-types.d.ts +1 -178
  30. package/types/interfaces/wasi-http-outgoing-handler.d.ts +2 -2
  31. package/types/interfaces/wasi-http-types.d.ts +412 -82
  32. package/types/interfaces/wasi-io-error.d.ts +16 -0
  33. package/types/interfaces/wasi-io-poll.d.ts +19 -8
  34. package/types/interfaces/wasi-io-streams.d.ts +26 -46
  35. package/types/interfaces/wasi-sockets-ip-name-lookup.d.ts +9 -21
  36. package/types/interfaces/wasi-sockets-network.d.ts +4 -0
  37. package/types/interfaces/wasi-sockets-tcp.d.ts +75 -18
  38. package/types/interfaces/wasi-sockets-udp-create-socket.d.ts +1 -1
  39. package/types/interfaces/wasi-sockets-udp.d.ts +282 -193
  40. package/types/wasi-cli-command.d.ts +28 -28
  41. package/types/wasi-http-proxy.d.ts +12 -12
  42. package/lib/common/io.js +0 -183
  43. package/lib/common/make-request.js +0 -30
  44. package/types/interfaces/wasi-clocks-timezone.d.ts +0 -56
@@ -0,0 +1,794 @@
1
+ /**
2
+ * @typedef {import("../../../types/interfaces/wasi-sockets-network.js").Network} Network
3
+ * @typedef {import("../../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress
4
+ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").TcpSocket} TcpSocket
5
+ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").InputStream} InputStream
6
+ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").OutputStream} OutputStream
7
+ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily
8
+ * @typedef {import("../../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable
9
+ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").ShutdownType} ShutdownType
10
+ * @typedef {import("../../../types/interfaces/wasi-clocks-monotonic-clock.js").Duration} Duration
11
+ */
12
+
13
+ import { isIP, Socket as NodeSocket } from "node:net";
14
+ import { platform } from "node:os";
15
+ import { assert } from "../../common/assert.js";
16
+ // import { streams } from "../io.js";
17
+ // const { InputStream, OutputStream } = streams;
18
+
19
+ const symbolDispose = Symbol.dispose || Symbol.for("dispose");
20
+ const symbolSocketState = Symbol.SocketInternalState || Symbol.for("SocketInternalState");
21
+ const symbolOperations = Symbol.SocketOperationsState || Symbol.for("SocketOperationsState");
22
+
23
+ // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc
24
+ const { TCP, TCPConnectWrap, constants: TCPConstants } = process.binding("tcp_wrap");
25
+ const { ShutdownWrap } = process.binding("stream_wrap");
26
+
27
+ import { INPUT_STREAM_CREATE, OUTPUT_STREAM_CREATE, SOCKET } from "../../io/calls.js";
28
+ import { inputStreamCreate, ioCall, outputStreamCreate, pollableCreate } from "../../io/worker-io.js";
29
+ import {
30
+ deserializeIpAddress,
31
+ findUnsuedLocalAddress,
32
+ isIPv4MappedAddress,
33
+ isMulticastIpAddress,
34
+ isUnicastIpAddress,
35
+ serializeIpAddress,
36
+ } from "./socket-common.js";
37
+
38
+ // TODO: move to a common
39
+ const ShutdownType = {
40
+ Receive: "receive",
41
+ Send: "send",
42
+ Both: "both",
43
+ };
44
+
45
+ // TODO: move to a common
46
+ const SocketConnectionState = {
47
+ Error: "Error",
48
+ Closed: "Closed",
49
+ Connecting: "Connecting",
50
+ Connected: "Connected",
51
+ Listening: "Listening",
52
+ };
53
+
54
+ // As a workaround, we store the bound address in a global map
55
+ // this is needed because 'address-in-use' is not always thrown when binding
56
+ // more than one socket to the same address
57
+ // TODO: remove this workaround when we figure out why!
58
+ const globalBoundAddresses = new Map();
59
+
60
+ // TODO: implement would-block exceptions
61
+ // TODO: implement concurrency-conflict exceptions
62
+ export class TcpSocketImpl {
63
+ id = 1;
64
+ /** @type {TCP.TCPConstants.SOCKET} */ #socket = null;
65
+ /** @type {Network} */ network = null;
66
+
67
+ #connections = 0;
68
+
69
+ #pollId = null;
70
+
71
+ // track in-progress operations
72
+ // counter must be 0 for the operation to be considered complete
73
+ // we increment the counter when the operation starts
74
+ // and decrement it when the operation finishes
75
+ [symbolOperations] = {
76
+ bind: 0,
77
+ connect: 0,
78
+ listen: 0,
79
+ accept: 0,
80
+ };
81
+
82
+ [symbolSocketState] = {
83
+ lastErrorState: null,
84
+ isBound: false,
85
+ ipv6Only: false,
86
+ connectionState: SocketConnectionState.Closed,
87
+ acceptedClient: null,
88
+ canReceive: true,
89
+ canSend: true,
90
+
91
+ // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31
92
+ backlogSize: 128,
93
+
94
+ // TODO: what these default values should be?
95
+ keepAlive: false,
96
+ keepAliveCount: 1,
97
+ keepAliveIdleTime: 1,
98
+ keepAliveInterval: 1,
99
+ hopLimit: 1,
100
+ receiveBufferSize: 1,
101
+ sendBufferSize: 1,
102
+ };
103
+
104
+ #socketOptions = {
105
+ family: "ipv4",
106
+ localAddress: "",
107
+ localPort: 0,
108
+ remoteAddress: "",
109
+ remotePort: 0,
110
+ };
111
+
112
+ // this is set by the TcpSocket child class
113
+ #tcpSocketChildClassType = null;
114
+
115
+ /**
116
+ * @param {IpAddressFamily} addressFamily
117
+ * @param {TcpSocket} childClassType
118
+ * @param {number} id
119
+ */
120
+ constructor(addressFamily, childClassType, id) {
121
+ this.id = id;
122
+
123
+ this.#socketOptions.family = addressFamily.toLocaleLowerCase();
124
+ this.#tcpSocketChildClassType = childClassType;
125
+
126
+ this.#socket = new TCP(TCPConstants.SOCKET | TCPConstants.SERVER);
127
+ }
128
+
129
+ #handleConnection(err, newClientSocket) {
130
+ if (err) {
131
+ assert(true, "unknown", err);
132
+ }
133
+
134
+ this.#connections++;
135
+
136
+ this[symbolSocketState].acceptedClient = new NodeSocket({
137
+ handle: newClientSocket,
138
+ });
139
+ this[symbolSocketState].acceptedClient.server = this.#socket;
140
+ this[symbolSocketState].acceptedClient._server = this.#socket;
141
+
142
+ // TODO: handle data received from the client
143
+ this[symbolSocketState].acceptedClient._handle.onread = (nread, buffer) => {
144
+ if (nread > 0) {
145
+ const data = buffer.toString("utf8", 0, nread);
146
+ console.log("accepted socket on read:", data);
147
+ }
148
+ };
149
+ }
150
+
151
+ #handleDisconnect(err) {
152
+ if (err) {
153
+ assert(true, "unknown", err);
154
+ }
155
+
156
+ this.#connections--;
157
+ }
158
+
159
+ #onClientConnectComplete(err) {
160
+ if (err) {
161
+ // TODO: figure out what theis error mean and why it is thrown
162
+ assert(err === -89, "-89"); // on macos
163
+
164
+ assert(err === -99, "ephemeral-ports-exhausted");
165
+ assert(err === -104, "connection-reset");
166
+ assert(err === -110, "timeout");
167
+ assert(err === -111, "connection-refused");
168
+ assert(err === -113, "remote-unreachable");
169
+ assert(err === -125, "operation-cancelled");
170
+
171
+ throw new Error(err);
172
+ }
173
+
174
+ this[symbolSocketState].connectionState = SocketConnectionState.Connected;
175
+ }
176
+
177
+ // TODO: is this needed?
178
+ #handleAfterShutdown() {}
179
+
180
+ #autoBind(network, ipFamily) {
181
+ const unsusedLocalAddress = findUnsuedLocalAddress(ipFamily);
182
+ this.#socketOptions.localAddress = serializeIpAddress(unsusedLocalAddress, this.#socketOptions.family);
183
+ this.#socketOptions.localPort = unsusedLocalAddress.val.port;
184
+ this.startBind(network, unsusedLocalAddress);
185
+ this.finishBind();
186
+ }
187
+
188
+ #cacheBoundAddress() {
189
+ let { localIpSocketAddress: boundAddress, localPort } = this.#socketOptions;
190
+ // when port is 0, the OS will assign an ephemeral port
191
+ // we need to get the actual port assigned by the OS
192
+ if (localPort === 0) {
193
+ boundAddress = this.localAddress();
194
+ }
195
+ globalBoundAddresses.set(serializeIpAddress(boundAddress, true), this.#socket);
196
+ }
197
+
198
+ /**
199
+ * @param {Network} network
200
+ * @param {IpSocketAddress} localAddress
201
+ * @returns {void}
202
+ * @throws {invalid-argument} The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows)
203
+ * @throws {invalid-argument} `local-address` is not a unicast address. (EINVAL)
204
+ * @throws {invalid-argument} `local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL)
205
+ * @throws {invalid-state} The socket is already bound. (EINVAL)
206
+ */
207
+ startBind(network, localAddress) {
208
+ try {
209
+ assert(this[symbolSocketState].isBound, "invalid-state", "The socket is already bound");
210
+
211
+ const address = serializeIpAddress(localAddress);
212
+ const ipFamily = `ipv${isIP(address)}`;
213
+
214
+ assert(
215
+ this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(),
216
+ "invalid-argument",
217
+ "The `local-address` has the wrong address family"
218
+ );
219
+
220
+ assert(isUnicastIpAddress(localAddress) === false, "invalid-argument");
221
+ assert(isIPv4MappedAddress(localAddress) && this.ipv6Only(), "invalid-argument");
222
+
223
+ const { port } = localAddress.val;
224
+ this.#socketOptions.localIpSocketAddress = localAddress;
225
+ this.#socketOptions.localAddress = address;
226
+ this.#socketOptions.localPort = port;
227
+ this.network = network;
228
+ this[symbolOperations].bind++;
229
+ this[symbolSocketState].lastErrorState = null;
230
+ } catch (err) {
231
+ this[symbolSocketState].lastErrorState = err;
232
+ throw err;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * @returns {void}
238
+ * @throws {address-in-use} No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows)
239
+ * @throws {address-in-use} Address is already in use. (EADDRINUSE)
240
+ * @throws {address-not-bindable} `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL)
241
+ * @throws {not-in-progress} A `bind` operation is not in progress.
242
+ * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
243
+ **/
244
+ finishBind() {
245
+ try {
246
+ assert(this[symbolOperations].bind === 0, "not-in-progress");
247
+
248
+ const { localAddress, localIpSocketAddress, localPort, family } = this.#socketOptions;
249
+ assert(isIP(localAddress) === 0, "address-not-bindable");
250
+ assert(globalBoundAddresses.has(serializeIpAddress(localIpSocketAddress, true)), "address-in-use");
251
+
252
+ let err = null;
253
+ let bind = "bind"; // ipv4
254
+ if (family.toLocaleLowerCase() === "ipv6") {
255
+ bind = "bind6";
256
+ }
257
+
258
+ err = this.#socket[bind](localAddress, localPort);
259
+
260
+ if (err) {
261
+ this.#socket.close();
262
+ assert(err === -22, "address-in-use");
263
+ assert(err === -49, "address-not-bindable");
264
+ assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL
265
+ assert(true, "unknown", err);
266
+ }
267
+
268
+ this[symbolSocketState].lastErrorState = null;
269
+ this[symbolSocketState].isBound = true;
270
+ this[symbolOperations].bind--;
271
+
272
+ this.#cacheBoundAddress();
273
+ } catch (err) {
274
+ this[symbolSocketState].lastErrorState = err;
275
+ throw err;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * @param {Network} network
281
+ * @param {IpSocketAddress} remoteAddress
282
+ * @returns {void}
283
+ * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT)
284
+ * @throws {invalid-argument} `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS)
285
+ * @throws {invalid-argument} `remote-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL, EADDRNOTAVAIL on Illumos)
286
+ * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa)
287
+ * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows)
288
+ * @throws {invalid-argument} The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows)
289
+ * @throws {invalid-argument} The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`.
290
+ * @throws {invalid-state} The socket is already in the Connection state. (EISCONN)
291
+ * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows)
292
+ */
293
+ startConnect(network, remoteAddress) {
294
+ const host = serializeIpAddress(remoteAddress);
295
+ const ipFamily = `ipv${isIP(host)}`;
296
+ try {
297
+ assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state");
298
+ assert(this[symbolSocketState].connectionState === SocketConnectionState.Connecting, "invalid-state");
299
+ assert(this[symbolSocketState].connectionState === SocketConnectionState.Listening, "invalid-state");
300
+
301
+ assert(host === "0.0.0.0" || host === "0:0:0:0:0:0:0:0", "invalid-argument");
302
+ assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument");
303
+ assert(isUnicastIpAddress(remoteAddress) === false, "invalid-argument");
304
+ assert(isMulticastIpAddress(remoteAddress), "invalid-argument");
305
+ assert(isIPv4MappedAddress(remoteAddress) && this.ipv6Only(), "invalid-argument");
306
+ assert(remoteAddress.val.port === 0, "invalid-argument");
307
+
308
+ if (this[symbolSocketState].isBound === false) {
309
+ this.#autoBind(network, ipFamily);
310
+ }
311
+
312
+ assert(network !== this.network, "invalid-argument");
313
+ assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument");
314
+ assert(remoteAddress.val.port === 0 && platform() === "win32", "invalid-argument");
315
+ } catch (err) {
316
+ this[symbolSocketState].lastErrorState = err;
317
+ throw err;
318
+ }
319
+
320
+ this[symbolSocketState].lastErrorState = null;
321
+
322
+ this.#socketOptions.remoteIpSocketAddress = remoteAddress;
323
+ this.#socketOptions.remoteAddress = host;
324
+ this.#socketOptions.remotePort = remoteAddress.val.port;
325
+ this.network = network;
326
+ this[symbolOperations].connect++;
327
+ }
328
+
329
+ /**
330
+ * @returns {Array<InputStream, OutputStream>}
331
+ * @throws {timeout} Connection timed out. (ETIMEDOUT)
332
+ * @throws {connection-refused} The connection was forcefully rejected. (ECONNREFUSED)
333
+ * @throws {connection-reset} The connection was reset. (ECONNRESET)
334
+ * @throws {connection-aborted} The connection was aborted. (ECONNABORTED)
335
+ * @throws {remote-unreachable} The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
336
+ * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD)
337
+ * @throws {not-in-progress} A `connect` operation is not in progress.
338
+ * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
339
+ */
340
+ finishConnect() {
341
+ try {
342
+ assert(this[symbolOperations].connect === 0, "not-in-progress");
343
+ } catch (err) {
344
+ this[symbolSocketState].lastErrorState = err;
345
+ throw err;
346
+ }
347
+
348
+ this[symbolSocketState].lastErrorState = null;
349
+
350
+ const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions;
351
+ const connectReq = new TCPConnectWrap();
352
+
353
+ let err = null;
354
+ let connect = "connect"; // ipv4
355
+ if (family.toLocaleLowerCase() === "ipv6") {
356
+ connect = "connect6";
357
+ }
358
+
359
+ err = this.#socket[connect](connectReq, remoteAddress, remotePort);
360
+
361
+ if (err) {
362
+ console.error(`[tcp] connect error on socket: ${err}`);
363
+ this[symbolSocketState].connectionState = SocketConnectionState.Error;
364
+ }
365
+
366
+ connectReq.oncomplete = this.#onClientConnectComplete.bind(this);
367
+ connectReq.address = remoteAddress;
368
+ connectReq.port = remotePort;
369
+ connectReq.localAddress = localAddress;
370
+ connectReq.localPort = localPort;
371
+
372
+ this.#socket.onread = (_buffer) => {
373
+ // TODO: handle data received from the server
374
+ };
375
+
376
+ this.#socket.readStart();
377
+
378
+ const inputStream = inputStreamCreate(SOCKET, ioCall(INPUT_STREAM_CREATE | SOCKET, null, {}));
379
+ const outputStream = outputStreamCreate(SOCKET, ioCall(OUTPUT_STREAM_CREATE | SOCKET, null, {}));
380
+
381
+ this[symbolOperations].connect--;
382
+ this[symbolSocketState].connectionState = SocketConnectionState.Connecting;
383
+
384
+ // TODO: this is a temporary workaround, move this to the connection callback
385
+ // when the connection is actually established
386
+ this[symbolSocketState].connectionState = SocketConnectionState.Connected;
387
+
388
+ return [inputStream, outputStream];
389
+ }
390
+
391
+ /**
392
+ * @returns {void}
393
+ * @throws {invalid-state} The socket is not bound to any local address. (EDESTADDRREQ)
394
+ * @throws {invalid-state} The socket is already in the Connection state. (EISCONN, EINVAL on BSD)
395
+ * @throws {invalid-state} The socket is already in the Listener state.
396
+ */
397
+ startListen() {
398
+ try {
399
+ assert(this[symbolSocketState].lastErrorState !== null, "invalid-state");
400
+ assert(this[symbolSocketState].isBound === false, "invalid-state");
401
+ assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state");
402
+ assert(this[symbolSocketState].connectionState === SocketConnectionState.Listening, "invalid-state");
403
+ } catch (err) {
404
+ this[symbolSocketState].lastErrorState = err;
405
+ throw err;
406
+ }
407
+
408
+ this[symbolSocketState].lastErrorState = null;
409
+ this[symbolOperations].listen++;
410
+ }
411
+
412
+ /**
413
+ * @returns {void}
414
+ * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE)
415
+ * @throws {not-in-progress} A `listen` operation is not in progress.
416
+ * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
417
+ */
418
+ finishListen() {
419
+ try {
420
+ assert(this[symbolOperations].listen === 0, "not-in-progress");
421
+ } catch (err) {
422
+ this[symbolSocketState].lastErrorState = err;
423
+ throw err;
424
+ }
425
+
426
+ this[symbolSocketState].lastErrorState = null;
427
+
428
+ const err = this.#socket.listen(this[symbolSocketState].backlogSize);
429
+ if (err) {
430
+ console.error(`[tcp] listen error on socket: ${err}`);
431
+ this.#socket.close();
432
+
433
+ // TODO: handle errors
434
+ throw new Error(err);
435
+ }
436
+
437
+ this[symbolSocketState].connectionState = SocketConnectionState.Listening;
438
+ this[symbolOperations].listen--;
439
+ }
440
+
441
+ /**
442
+ * @returns {Array<TcpSocket, InputStream, OutputStream>}
443
+ * @throws {invalid-state} Socket is not in the Listener state. (EINVAL)
444
+ * @throws {would-block} No pending connections at the moment. (EWOULDBLOCK, EAGAIN)
445
+ * @throws {connection-aborted} An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED)
446
+ * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE)
447
+ */
448
+ accept() {
449
+ this[symbolOperations].accept++;
450
+
451
+ try {
452
+ assert(this[symbolSocketState].connectionState !== SocketConnectionState.Listening, "invalid-state");
453
+ } catch (err) {
454
+ this[symbolSocketState].lastErrorState = err;
455
+ throw err;
456
+ }
457
+
458
+ this[symbolSocketState].lastErrorState = null;
459
+
460
+ if (this[symbolSocketState].isBound === false) {
461
+ this.#autoBind(this.network, this.addressFamily());
462
+ }
463
+ const inputStream = inputStreamCreate(SOCKET, ioCall(INPUT_STREAM_CREATE | SOCKET, null, {}));
464
+ const outputStream = outputStreamCreate(SOCKET, ioCall(OUTPUT_STREAM_CREATE | SOCKET, null, {}));
465
+
466
+ // Because we have to return a valid TcpSocket resrouce type,
467
+ // we need to instantiate the correct child class
468
+ // TODO: figure out a more elegant way to do this
469
+ const socket = new this.#tcpSocketChildClassType(this.addressFamily());
470
+
471
+ // The returned socket is bound and in the Connection state.
472
+ // The following properties are inherited from the listener socket:
473
+ // - `address-family`
474
+ // - `ipv6-only`
475
+ // - `keep-alive-enabled`
476
+ // - `keep-alive-idle-time`
477
+ // - `keep-alive-interval`
478
+ // - `keep-alive-count`
479
+ // - `hop-limit`
480
+ // - `receive-buffer-size`
481
+ // - `send-buffer-size`
482
+ //
483
+ socket[symbolSocketState].ipv6Only = this[symbolSocketState].ipv6Only;
484
+ socket[symbolSocketState].keepAlive = this[symbolSocketState].keepAlive;
485
+ socket[symbolSocketState].keepAliveIdleTime = this[symbolSocketState].keepAliveIdleTime;
486
+ socket[symbolSocketState].keepAliveInterval = this[symbolSocketState].keepAliveInterval;
487
+ socket[symbolSocketState].keepAliveCount = this[symbolSocketState].keepAliveCount;
488
+ socket[symbolSocketState].hopLimit = this[symbolSocketState].hopLimit;
489
+ socket[symbolSocketState].receiveBufferSize = this[symbolSocketState].receiveBufferSize;
490
+ socket[symbolSocketState].sendBufferSize = this[symbolSocketState].sendBufferSize;
491
+
492
+ this[symbolOperations].accept--;
493
+
494
+ return [socket, inputStream, outputStream];
495
+ }
496
+
497
+ /**
498
+ * @returns {IpSocketAddress}
499
+ * @throws {invalid-state} The socket is not bound to any local address.
500
+ */
501
+ localAddress() {
502
+ assert(this[symbolSocketState].isBound === false, "invalid-state");
503
+
504
+ const out = {};
505
+ this.#socket.getsockname(out);
506
+
507
+ const { address, port, family } = out;
508
+ this.#socketOptions.localAddress = address;
509
+ this.#socketOptions.localPort = port;
510
+ this.#socketOptions.family = family.toLocaleLowerCase();
511
+
512
+ return {
513
+ tag: family.toLocaleLowerCase(),
514
+ val: {
515
+ address: deserializeIpAddress(address, family),
516
+ port,
517
+ },
518
+ };
519
+ }
520
+
521
+ /**
522
+ * @returns {IpSocketAddress}
523
+ * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN)
524
+ */
525
+ remoteAddress() {
526
+ assert(this[symbolSocketState].connectionState !== SocketConnectionState.Connected, "invalid-state");
527
+
528
+ const out = {};
529
+ this.#socket.getpeername(out);
530
+
531
+ const { address, port, family } = out;
532
+ this.#socketOptions.remoteAddress = address;
533
+ this.#socketOptions.remotePort = port;
534
+ this.#socketOptions.family = family.toLocaleLowerCase();
535
+
536
+ return {
537
+ tag: family.toLocaleLowerCase(),
538
+ val: {
539
+ address: deserializeIpAddress(address, family),
540
+ port,
541
+ },
542
+ };
543
+ }
544
+
545
+ isListening() {
546
+ return this[symbolSocketState].connectionState === SocketConnectionState.Listening;
547
+ }
548
+
549
+ /**
550
+ * @returns {IpAddressFamily}
551
+ */
552
+ addressFamily() {
553
+ return this.#socketOptions.family;
554
+ }
555
+
556
+ /**
557
+ * @returns {boolean}
558
+ * @throws {not-supported} (get/set) `this` socket is an IPv4 socket.
559
+ */
560
+ ipv6Only() {
561
+ assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported");
562
+
563
+ return this[symbolSocketState].ipv6Only;
564
+ }
565
+
566
+ /**
567
+ * @param {boolean} value
568
+ * @returns {void}
569
+ * @throws {invalid-state} (set) The socket is already bound.
570
+ * @throws {invalid-state} (get/set) `this` socket is an IPv4 socket.
571
+ * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.)
572
+ */
573
+ setIpv6Only(value) {
574
+ assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported");
575
+ assert(this[symbolSocketState].isBound, "invalid-state");
576
+
577
+ this[symbolSocketState].ipv6Only = value;
578
+ }
579
+
580
+ /**
581
+ * @param {bigint} value
582
+ * @returns {void}
583
+ * @throws {not-supported} (set) The platform does not support changing the backlog size after the initial listen.
584
+ * @throws {invalid-argument} (set) The provided value was 0.
585
+ * @throws {invalid-state} (set) The socket is already in the Connection state.
586
+ */
587
+ setListenBacklogSize(value) {
588
+ assert(value === 0n, "invalid-argument", "The provided value was 0.");
589
+ assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state");
590
+
591
+ this[symbolSocketState].backlogSize = Number(value);
592
+ }
593
+
594
+ /**
595
+ * @returns {boolean}
596
+ */
597
+ keepAliveEnabled() {
598
+ return this[symbolSocketState].keepAlive;
599
+ }
600
+
601
+ /**
602
+ * @param {boolean} value
603
+ * @returns {void}
604
+ */
605
+ setKeepAliveEnabled(value) {
606
+ this.#socket.setKeepAlive(value);
607
+ this[symbolSocketState].keepAlive = value;
608
+
609
+ if (value) {
610
+ this.setKeepAliveIdleTime(this.keepAliveIdleTime());
611
+ this.setKeepAliveInterval(this.keepAliveInterval());
612
+ this.setKeepAliveCount(this.keepAliveCount());
613
+ }
614
+ }
615
+
616
+ /**
617
+ *
618
+ * @returns {Duration}
619
+ */
620
+ keepAliveIdleTime() {
621
+ return this[symbolSocketState].keepAliveIdleTime;
622
+ }
623
+
624
+ /**
625
+ *
626
+ * @param {Duration} value
627
+ * @returns {void}
628
+ * @throws {invalid-argument} (set) The idle time must be 1 or higher.
629
+ */
630
+ setKeepAliveIdleTime(value) {
631
+ assert(value < 1, "invalid-argument", "The idle time must be 1 or higher.");
632
+
633
+ this[symbolSocketState].keepAliveIdleTime = value;
634
+ }
635
+
636
+ /**
637
+ *
638
+ * @returns {Duration}
639
+ */
640
+ keepAliveInterval() {
641
+ return this[symbolSocketState].keepAliveInterval;
642
+ }
643
+
644
+ /**
645
+ *
646
+ * @param {Duration} value
647
+ * @returns {void}
648
+ * @throws {invalid-argument} (set) The interval must be 1 or higher.
649
+ */
650
+ setKeepAliveInterval(value) {
651
+ assert(value < 1, "invalid-argument", "The interval must be 1 or higher.");
652
+
653
+ this[symbolSocketState].keepAliveInterval = value;
654
+ }
655
+
656
+ /**
657
+ *
658
+ * @returns {Duration}
659
+ */
660
+ keepAliveCount() {
661
+ return this[symbolSocketState].keepAliveCount;
662
+ }
663
+
664
+ /**
665
+ *
666
+ * @param {Duration} value
667
+ * @returns {void}
668
+ * @throws {invalid-argument} (set) The count must be 1 or higher.
669
+ */
670
+ setKeepAliveCount(value) {
671
+ assert(value < 1, "invalid-argument", "The count must be 1 or higher.");
672
+
673
+ // TODO: set this on the client socket as well
674
+ this[symbolSocketState].keepAliveCount = value;
675
+ }
676
+
677
+ /**
678
+ * @returns {number}
679
+ * @description Not available on Node.js (see https://github.com/WebAssembly/wasi-sockets/blob/main/Posix-compatibility.md#socket-options)
680
+ */
681
+ hopLimit() {
682
+ return this[symbolSocketState].hopLimit;
683
+ }
684
+
685
+ /**
686
+ * @param {number} value
687
+ * @returns {void}
688
+ * @throws {invalid-argument} (set) The TTL value must be 1 or higher.
689
+ * @throws {invalid-state} (set) The socket is already in the Connection state.
690
+ * @throws {invalid-state} (set) The socket is already in the Listener state.
691
+ * @description Not available on Node.js (see https://github.com/WebAssembly/wasi-sockets/blob/main/Posix-compatibility.md#socket-options)
692
+ */
693
+ setHopLimit(value) {
694
+ assert(value < 1, "invalid-argument", "The TTL value must be 1 or higher.");
695
+
696
+ this[symbolSocketState].hopLimit = value;
697
+ }
698
+
699
+ /**
700
+ * @returns {bigint}
701
+ */
702
+ receiveBufferSize() {
703
+ return this[symbolSocketState].receiveBufferSize;
704
+ }
705
+
706
+ /**
707
+ * @param {number} value
708
+ * @returns {void}
709
+ * @throws {not-supported} (set) The platform does not support changing the backlog size after the initial listen.
710
+ * @throws {invalid-argument} (set) The provided value was 0.
711
+ * @throws {invalid-state} (set) The socket is already in the Connection state.
712
+ */
713
+ setReceiveBufferSize(value) {
714
+ // TODO: review these assertions based on WIT specs
715
+ // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state");
716
+ assert(value === 0n, "invalid-argument", "The provided value was 0.");
717
+
718
+ // TODO: set this on the client socket as well
719
+ this[symbolSocketState].receiveBufferSize = value;
720
+ }
721
+
722
+ /**
723
+ * @returns {bigint}
724
+ */
725
+ sendBufferSize() {
726
+ return this[symbolSocketState].sendBufferSize;
727
+ }
728
+
729
+ /**
730
+ * @param {bigint} value
731
+ * @returns {void}
732
+ * @throws {invalid-argument} (set) The provided value was 0.
733
+ * @throws {invalid-state} (set) The socket is already in the Connection state.
734
+ * @throws {invalid-state} (set) The socket is already in the Listener state.
735
+ */
736
+ setSendBufferSize(value) {
737
+ // TODO: review these assertions based on WIT specs
738
+ // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state");
739
+ assert(value === 0n, "invalid-argument", "The provided value was 0.");
740
+
741
+ // TODO: set this on the client socket as well
742
+ this[symbolSocketState].sendBufferSize = value;
743
+ }
744
+
745
+ /**
746
+ * @returns {Pollable}
747
+ */
748
+ subscribe() {
749
+ if (this.#pollId) return pollableCreate(this.#pollId);
750
+ // 0 poll is immediately resolving
751
+ return pollableCreate(0);
752
+ }
753
+
754
+ /**
755
+ * @param {ShutdownType} shutdownType
756
+ * @returns {void}
757
+ * @throws {invalid-state} The socket is not in the Connection state. (ENOTCONN)
758
+ */
759
+ shutdown(shutdownType) {
760
+ assert(this[symbolSocketState].connectionState !== SocketConnectionState.Connected, "invalid-state");
761
+
762
+ // TODO: figure out how to handle shutdownTypes
763
+ if (shutdownType === ShutdownType.Receive) {
764
+ this[symbolSocketState].canReceive = false;
765
+ } else if (shutdownType === ShutdownType.Send) {
766
+ this[symbolSocketState].canSend = false;
767
+ } else if (shutdownType === ShutdownType.Both) {
768
+ this[symbolSocketState].canReceive = false;
769
+ this[symbolSocketState].canSend = false;
770
+ }
771
+
772
+ const req = new ShutdownWrap();
773
+ req.oncomplete = this.#handleAfterShutdown.bind(this);
774
+ req.handle = this._handle;
775
+ req.callback = () => {};
776
+ const err = this._handle.shutdown(req);
777
+
778
+ assert(err === 1, "invalid-state");
779
+ }
780
+
781
+ [symbolDispose]() {
782
+ this.#socket.close();
783
+
784
+ // we only need to remove the bound address from the global map
785
+ // if the socket was already bound
786
+ if (this[symbolSocketState].isBound) {
787
+ globalBoundAddresses.delete(serializeIpAddress(this.#socketOptions.localIpSocketAddress, true));
788
+ }
789
+ }
790
+
791
+ handle() {
792
+ return this.#socket;
793
+ }
794
+ }