@bytecodealliance/preview3-shim 0.1.0-rc.0

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 (67) hide show
  1. package/LICENSE +220 -0
  2. package/README.md +14 -0
  3. package/lib/nodejs/cli.js +127 -0
  4. package/lib/nodejs/clocks.js +100 -0
  5. package/lib/nodejs/filesystem/descriptor.js +997 -0
  6. package/lib/nodejs/filesystem/error.js +92 -0
  7. package/lib/nodejs/filesystem/utils.js +19 -0
  8. package/lib/nodejs/filesystem.js +2 -0
  9. package/lib/nodejs/finalization.js +33 -0
  10. package/lib/nodejs/future.js +187 -0
  11. package/lib/nodejs/http/client.js +146 -0
  12. package/lib/nodejs/http/error.js +71 -0
  13. package/lib/nodejs/http/fields.js +359 -0
  14. package/lib/nodejs/http/request.js +589 -0
  15. package/lib/nodejs/http/response.js +212 -0
  16. package/lib/nodejs/http/server.js +157 -0
  17. package/lib/nodejs/http.js +34 -0
  18. package/lib/nodejs/index.js +8 -0
  19. package/lib/nodejs/random.js +14 -0
  20. package/lib/nodejs/sockets/address.js +146 -0
  21. package/lib/nodejs/sockets/error.js +123 -0
  22. package/lib/nodejs/sockets/ip-name-lookup.js +93 -0
  23. package/lib/nodejs/sockets/tcp.js +776 -0
  24. package/lib/nodejs/sockets/udp.js +516 -0
  25. package/lib/nodejs/sockets.js +14 -0
  26. package/lib/nodejs/stream.js +423 -0
  27. package/lib/nodejs/workers/cli-worker.js +49 -0
  28. package/lib/nodejs/workers/filesystem-worker.js +119 -0
  29. package/lib/nodejs/workers/http-worker.js +387 -0
  30. package/lib/nodejs/workers/resource-worker.js +151 -0
  31. package/lib/nodejs/workers/tcp-worker.js +283 -0
  32. package/lib/nodejs/workers/udp-worker.js +194 -0
  33. package/package.json +57 -0
  34. package/types/cli.d.ts +12 -0
  35. package/types/clocks.d.ts +2 -0
  36. package/types/filesystem.d.ts +2 -0
  37. package/types/http.d.ts +4 -0
  38. package/types/index.d.ts +6 -0
  39. package/types/interfaces/wasi-cli-environment.d.ts +21 -0
  40. package/types/interfaces/wasi-cli-exit.d.ts +6 -0
  41. package/types/interfaces/wasi-cli-run.d.ts +5 -0
  42. package/types/interfaces/wasi-cli-stderr.d.ts +14 -0
  43. package/types/interfaces/wasi-cli-stdin.d.ts +19 -0
  44. package/types/interfaces/wasi-cli-stdout.d.ts +14 -0
  45. package/types/interfaces/wasi-cli-terminal-input.d.ts +8 -0
  46. package/types/interfaces/wasi-cli-terminal-output.d.ts +8 -0
  47. package/types/interfaces/wasi-cli-terminal-stderr.d.ts +7 -0
  48. package/types/interfaces/wasi-cli-terminal-stdin.d.ts +7 -0
  49. package/types/interfaces/wasi-cli-terminal-stdout.d.ts +7 -0
  50. package/types/interfaces/wasi-cli-types.d.ts +15 -0
  51. package/types/interfaces/wasi-clocks-monotonic-clock.d.ts +33 -0
  52. package/types/interfaces/wasi-clocks-system-clock.d.ts +38 -0
  53. package/types/interfaces/wasi-clocks-types.d.ts +5 -0
  54. package/types/interfaces/wasi-filesystem-preopens.d.ts +6 -0
  55. package/types/interfaces/wasi-filesystem-types.d.ts +780 -0
  56. package/types/interfaces/wasi-http-client.d.ts +9 -0
  57. package/types/interfaces/wasi-http-handler.d.ts +9 -0
  58. package/types/interfaces/wasi-http-types.d.ts +619 -0
  59. package/types/interfaces/wasi-random-insecure-seed.d.ts +21 -0
  60. package/types/interfaces/wasi-random-insecure.d.ts +26 -0
  61. package/types/interfaces/wasi-random-random.d.ts +31 -0
  62. package/types/interfaces/wasi-sockets-ip-name-lookup.d.ts +77 -0
  63. package/types/interfaces/wasi-sockets-types.d.ts +830 -0
  64. package/types/random.d.ts +3 -0
  65. package/types/sockets.d.ts +2 -0
  66. package/types/wasi-cli-command.d.ts +23 -0
  67. package/types/wasi-http-service.d.ts +14 -0
@@ -0,0 +1,776 @@
1
+ import { StreamReader, readableByteStreamFromReader } from "../stream.js";
2
+ import { FutureReader } from "../future.js";
3
+ import { ResourceWorker } from "../workers/resource-worker.js";
4
+ import { SocketError } from "./error.js";
5
+
6
+ import {
7
+ isWildcardIpAddress,
8
+ isUnicastIpAddress,
9
+ isMulticastIpAddress,
10
+ isIpv4MappedAddress,
11
+ isBindableIpAddress,
12
+ IP_ADDRESS_FAMILY,
13
+ } from "./address.js";
14
+
15
+ let WORKER = null;
16
+ function worker() {
17
+ return (WORKER ??= new ResourceWorker(new URL("../workers/tcp-worker.js", import.meta.url)));
18
+ }
19
+
20
+ let TCP_CREATE_TOKEN = null;
21
+ function token() {
22
+ return (TCP_CREATE_TOKEN ??= Symbol("TcpCreateToken"));
23
+ }
24
+
25
+ const STATE = {
26
+ UNBOUND: "unbound",
27
+ BOUND: "bound",
28
+ LISTENING: "listening",
29
+ CONNECTING: "connecting",
30
+ CONNECTED: "connected",
31
+ CLOSED: "closed",
32
+ };
33
+
34
+ /**
35
+ * Creates a new TCP socket
36
+ * WIT: createTcpSocket: func(address-family: ip-address-family) -> result<tcp-socket, error-code>
37
+ *
38
+ * @param {IP_ADDRESS_FAMILY} addressFamily - The IP address family (IPv4 or IPv6)
39
+ * @returns {TcpSocket>} A new TCP socket instance
40
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `addressFamily` is not IPV4 or IPV6
41
+ * @throws {SocketError} for other errors, payload.tag maps the low-level system error_
42
+ */
43
+ export function createTcpSocket(addressFamily) {
44
+ if (addressFamily !== IP_ADDRESS_FAMILY.IPV4 && addressFamily !== IP_ADDRESS_FAMILY.IPV6) {
45
+ throw new SocketError("invalid-argument");
46
+ }
47
+
48
+ try {
49
+ const result = worker().runSync({
50
+ op: "tcp-create",
51
+ family: addressFamily,
52
+ });
53
+
54
+ return TcpSocket._create(token(), addressFamily, result.socketId);
55
+ } catch (error) {
56
+ throw SocketError.from(error);
57
+ }
58
+ }
59
+
60
+ export class TcpSocket {
61
+ #socketId = null;
62
+ #family = null;
63
+ #state = "unbound";
64
+ #options = {
65
+ // defaults per https://nodejs.org/docs/latest/api/net.html#socketsetkeepaliveenable-initialdelay
66
+ keepAliveEnabled: false,
67
+
68
+ // Node.js doesn't give us the ability to detect the OS default,
69
+ // therefore we hardcode the default value instead of using the OS default,
70
+ // since we would never be able to report it as a return value otherwise.
71
+ // We could make this configurable as a global JCO implementation configuration
72
+ // instead.
73
+ keepAliveIdleTime: 7200_000_000_000n,
74
+
75
+ // The following options are NOT configurable in Node.js!
76
+ // Any configurations set will respond correctly, but underneath retain
77
+ // system / Node.js defaults.
78
+ keepAliveInterval: 1_000_000_000n,
79
+ keepAliveCount: 10,
80
+ hopLimit: 1,
81
+ receiveBufferSize: 65_536n,
82
+ sendBufferSize: 65_536n,
83
+ };
84
+
85
+ /**
86
+ * WIT static constructor for `wasi:sockets/types.tcp-socket`.
87
+ *
88
+ * WIT:
89
+ * ```
90
+ * create: static func(address-family: ip-address-family) -> result<tcp-socket, error-code>
91
+ * ```
92
+ *
93
+ * @static
94
+ * @param {IP_ADDRESS_FAMILY} addressFamily - The IP address family (IPv4 or IPv6)
95
+ * @returns {TcpSocket} A new TcpSocket instance
96
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `addressFamily` is not IPV4 or IPV6
97
+ * @throws {SocketError} for other socket creation errors
98
+ */
99
+ static create(addressFamily) {
100
+ return createTcpSocket(addressFamily);
101
+ }
102
+
103
+ /**
104
+ * Creates a new TCP socket
105
+ * WIT:
106
+ * ```
107
+ * constructor(address-family: ip-address-family)
108
+ * ```
109
+ *
110
+ * @static
111
+ * @param {IP_ADDRESS_FAMILY} addressFamily - The IP address family (IPv4 or IPv6)
112
+ * @param {number} socketId - The internal socket identifier
113
+ * @returns {TcpSocket} A new TcpSocket instance
114
+ * @private
115
+ */
116
+ static _create(t, addressFamily, socketId) {
117
+ if (t !== token()) {
118
+ throw new Error("Use createTcpSocket to create a Socket");
119
+ }
120
+
121
+ if (addressFamily !== IP_ADDRESS_FAMILY.IPV4 && addressFamily !== IP_ADDRESS_FAMILY.IPV6) {
122
+ throw new SocketError("invalid-argument");
123
+ }
124
+
125
+ const socket = new TcpSocket();
126
+ socket.#family = addressFamily;
127
+ socket.#socketId = socketId;
128
+ return socket;
129
+ }
130
+
131
+ /**
132
+ * Binds the socket to a local address
133
+ * WIT:
134
+ * ```
135
+ * bind: func(local-address: ip-socket-address) -> result<_, error-code>
136
+ * ```
137
+ *
138
+ * @param {Object} localAddress - The local address to bind to
139
+ * @returns {void}
140
+ * @throws {SocketError} with payload.tag 'invalid-state' if the socket is not in ~STATE.UNBOUND~
141
+ * @throws {SocketError} with payload.tag 'invalid-argument' if ~localAddress~ is invalid
142
+ * @throws {SocketError} for other errors, payload.tag maps the system error
143
+ */
144
+ bind(localAddress) {
145
+ if (this.#state !== STATE.UNBOUND) {
146
+ throw new SocketError("invalid-state");
147
+ }
148
+
149
+ if (!this.#isValidLocalAddress(localAddress)) {
150
+ throw new SocketError("invalid-argument");
151
+ }
152
+ if (!isBindableIpAddress(localAddress)) {
153
+ throw new SocketError("address-not-bindable");
154
+ }
155
+ try {
156
+ worker().runSync({
157
+ op: "tcp-bind",
158
+ localAddress,
159
+ socketId: this.#socketId,
160
+ });
161
+
162
+ this.#state = STATE.BOUND;
163
+ } catch (error) {
164
+ throw SocketError.from(error);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Connects to a remote endpoint
170
+ * WIT:
171
+ * ```
172
+ * connect: async func(remote-address: ip-socket-address) -> result<_, error-code>
173
+ * ```
174
+ *
175
+ * @async
176
+ * @param {Object} remoteAddress - The remote address to connect to
177
+ * @returns {Promise<void>}
178
+ * @throws {SocketError} with payload.tag 'invalid-state' if socket is CONNECTING, CONNECTED, or LISTENING
179
+ * @throws {SocketError} with payload.tag 'invalid-argument' if ~remoteAddress~ is invalid or port == 0
180
+ * @throws {SocketError} for other errors, payload.tag maps the system error
181
+ */
182
+ async connect(remoteAddress) {
183
+ if (
184
+ this.#state === STATE.CONNECTING ||
185
+ this.#state === STATE.CONNECTED ||
186
+ this.#state === STATE.LISTENING
187
+ ) {
188
+ throw new SocketError("invalid-state");
189
+ }
190
+
191
+ if (!this.#isValidRemoteAddress(remoteAddress)) {
192
+ throw new SocketError("invalid-argument");
193
+ }
194
+
195
+ this.#state = STATE.CONNECTING;
196
+
197
+ try {
198
+ await worker().run({
199
+ op: "tcp-connect",
200
+ remoteAddress,
201
+ socketId: this.#socketId,
202
+ });
203
+
204
+ this.#state = STATE.CONNECTED;
205
+ } catch (error) {
206
+ this.#state = STATE.CLOSED;
207
+ throw SocketError.from(error);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Starts listening for connections
213
+ * WIT:
214
+ * ```
215
+ * listen: func() -> result<stream<tcp-socket>, error-code>
216
+ * ```
217
+ *
218
+ * @returns {StreamReader} A stream of incoming TCP socket connections
219
+ * @throws {SocketError} with payload.tag 'invalid-state' if socket is LISTENING or CONNECTED
220
+ * @throws {SocketError} for other errors, payload.tag maps the system error
221
+ */
222
+ listen() {
223
+ if (this.#state === STATE.LISTENING || this.#state === STATE.CONNECTED) {
224
+ throw new SocketError("invalid-state");
225
+ }
226
+ try {
227
+ // Convert incoming connections from worker to TcpSocket instances
228
+ const listener = this;
229
+ const transform = new TransformStream({
230
+ transform({ family, socketId }, controller) {
231
+ const socket = TcpSocket._create(token(), family, socketId);
232
+ socket.#state = STATE.CONNECTED;
233
+ socket.#options = { ...listener.#options };
234
+ controller.enqueue(socket);
235
+ },
236
+ });
237
+
238
+ worker().runSync(
239
+ {
240
+ op: "tcp-listen",
241
+ socketId: this.#socketId,
242
+ stream: transform.writable,
243
+ },
244
+ [transform.writable],
245
+ );
246
+
247
+ this.#state = STATE.LISTENING;
248
+ return new StreamReader(transform.readable);
249
+ } catch (error) {
250
+ this.#state = STATE.CLOSED;
251
+ throw SocketError.from(error);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Sends data to the connected peer
257
+ * WIT:
258
+ * ```
259
+ * send: async func(data: stream<u8>) -> result<_, error-code>
260
+ * ```
261
+ *
262
+ * @async
263
+ * @param {object} data - The byte stream to send.
264
+ * @returns {Promise<void>}
265
+ * @throws {SocketError} With payload.tag 'invalid-state' if socket is not CONNECTED
266
+ * @throws {SocketError} With payload.tag 'invalid-argument' if `data` is not a readable byte stream
267
+ * @throws {SocketError} for other errors, payload.tag maps the system error
268
+ */
269
+ async send(data) {
270
+ if (this.#state !== STATE.CONNECTED) {
271
+ throw new SocketError("invalid-state");
272
+ }
273
+ let stream;
274
+ try {
275
+ stream = readableByteStreamFromReader(data, { name: "tcp send data" });
276
+ } catch (error) {
277
+ if (error instanceof TypeError) {
278
+ throw new SocketError("invalid-argument", error.message, undefined, { cause: error });
279
+ }
280
+ throw error;
281
+ }
282
+
283
+ try {
284
+ // Transfer the stream to the worker
285
+ await worker().run(
286
+ {
287
+ op: "tcp-send",
288
+ socketId: this.#socketId,
289
+ stream,
290
+ },
291
+ [stream],
292
+ );
293
+ } catch (error) {
294
+ throw SocketError.from(error);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Receives data from the connected peer
300
+ * WIT:
301
+ * ```
302
+ * receive: func() -> tuple<stream<u8>, future<result<_, error-code>>>
303
+ * ```
304
+ *
305
+ * @throws {SocketError} with payload.tag 'invalid-state' if socket is not CONNECTED
306
+ * @throws {SocketError} for other errors, payload.tag maps the system error
307
+ * @returns {{ stream: StreamReader, future: FutureReader }}
308
+ */
309
+ receive() {
310
+ if (this.#state !== STATE.CONNECTED) {
311
+ throw new SocketError("invalid-state");
312
+ }
313
+
314
+ const transform = new TransformStream();
315
+ const promise = worker()
316
+ .run(
317
+ {
318
+ op: "tcp-receive",
319
+ socketId: this.#socketId,
320
+ stream: transform.writable,
321
+ },
322
+ [transform.writable],
323
+ )
324
+ .catch((err) => {
325
+ throw SocketError.from(err);
326
+ });
327
+
328
+ return [new StreamReader(transform.readable), new FutureReader(promise)];
329
+ }
330
+
331
+ /**
332
+ * Gets the bound local address
333
+ * WIT:
334
+ * ```
335
+ * local-address: func() -> result<ip-socket-address, error-code>
336
+ * ```
337
+ *
338
+ * @returns {Object} The local socket address
339
+ * @throws {SocketError} with payload.tag 'invalid-state' if socket is not BOUND
340
+ * @throws {SocketError} for other errors, payload.tag maps the system error
341
+ */
342
+ getLocalAddress() {
343
+ if (this.#state === STATE.UNBOUND) {
344
+ throw new SocketError("invalid-state");
345
+ }
346
+
347
+ try {
348
+ return worker().runSync({
349
+ op: "tcp-get-local-address",
350
+ socketId: this.#socketId,
351
+ });
352
+ } catch (err) {
353
+ throw SocketError.from(err);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Gets the remote address
359
+ * WIT:
360
+ * ```
361
+ * remote-address: func() -> result<ip-socket-address, error-code>
362
+ * ```
363
+ *
364
+ * @returns {Object} The remote socket address
365
+ * @throws {SocketError} with payload.tag 'invalid-state' if socket is not CONNECTED
366
+ * @throws {SocketError} for other errors, payload.tag maps the system error
367
+ */
368
+ getRemoteAddress() {
369
+ if (this.#state !== STATE.CONNECTED) {
370
+ throw new SocketError("invalid-state");
371
+ }
372
+
373
+ try {
374
+ return worker().runSync({
375
+ op: "tcp-get-remote-address",
376
+ socketId: this.#socketId,
377
+ });
378
+ } catch (err) {
379
+ throw SocketError.from(err);
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Checks if the socket is in listening state
385
+ * WIT:
386
+ * ```
387
+ * is-listening: func() -> bool
388
+ * ```
389
+ *
390
+ * @returns {boolean} True if socket is listening, false otherwise
391
+ */
392
+ getIsListening() {
393
+ return this.#state === STATE.LISTENING;
394
+ }
395
+
396
+ /**
397
+ * Gets the address family
398
+ * WIT:
399
+ * ```
400
+ * address-family: func() -> ip-address-family
401
+ * ```
402
+ *
403
+ * @returns {IP_ADDRESS_FAMILY} The IP address family (IPv4 or IPv6)
404
+ */
405
+ getAddressFamily() {
406
+ return this.#family;
407
+ }
408
+
409
+ /**
410
+ * Sets the listen backlog size
411
+ * WIT:
412
+ * ```
413
+ * set-listen-backlog-size: func(value: u64) -> result<_, error-code>
414
+ * ```
415
+ *
416
+ * @param {bigint} value - The backlog size
417
+ * @returns {void}
418
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `value` is 0
419
+ * @throws {SocketError} with payload.tag 'not-supported' if socket is CONNECTING or CONNECTED
420
+ * @throws {SocketError} with payload.tag 'invalid-state' if socket is not UNBOUND or BOUND
421
+ * @throws {SocketError} for other errors, payload.tag maps the system error
422
+ */
423
+ setListenBacklogSize(value) {
424
+ if (value <= 0n) {
425
+ throw new SocketError("invalid-argument");
426
+ }
427
+
428
+ if (
429
+ this.#state === STATE.CONNECTING ||
430
+ this.#state === STATE.CONNECTED ||
431
+ this.#state === STATE.LISTENING
432
+ ) {
433
+ throw new SocketError("not-supported");
434
+ }
435
+
436
+ if (this.#state !== STATE.UNBOUND && this.#state !== STATE.BOUND) {
437
+ throw new SocketError("invalid-state");
438
+ }
439
+
440
+ this.#options.listenBacklogSize = value;
441
+ try {
442
+ worker().runSync({
443
+ op: "tcp-set-listen-backlog-size",
444
+ socketId: this.#socketId,
445
+ value,
446
+ });
447
+ } catch (err) {
448
+ throw SocketError.from(err);
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Gets whether keep-alive is enabled
454
+ * WIT:
455
+ * ```
456
+ * keep-alive-enabled: func() -> result<bool, error-code>
457
+ * ```
458
+ *
459
+ * @returns {boolean} True if keep-alive is enabled, false otherwise
460
+ */
461
+ getKeepAliveEnabled() {
462
+ return this.#options.keepAliveEnabled;
463
+ }
464
+
465
+ /**
466
+ * Sets keep-alive enabled state
467
+ * WIT:
468
+ * ```
469
+ * set-keep-alive-enabled: func(value: bool) -> result<_, error-code>
470
+ * ```
471
+ *
472
+ * @async
473
+ * @param {boolean} value - Whether to enable keep-alive
474
+ * @returns {void}
475
+ * @throws {SocketError} for other errors, payload.tag maps the system error
476
+ */
477
+ setKeepAliveEnabled(value) {
478
+ try {
479
+ this.#options.keepAliveEnabled = value;
480
+
481
+ worker().runSync({
482
+ op: "tcp-set-keep-alive",
483
+ socketId: this.#socketId,
484
+ keepAliveEnabled: this.#options.keepAliveEnabled,
485
+ keepAliveIdleTime: this.#options.keepAliveIdleTime,
486
+ });
487
+ } catch (err) {
488
+ throw SocketError.from(err);
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Gets the keep-alive idle time
494
+ * WIT:
495
+ * ```
496
+ * keep-alive-idle-time: func() -> result<duration, error-code>
497
+ * ```
498
+ *
499
+ * @returns {bigint} The keep-alive idle time in nanoseconds
500
+ */
501
+ getKeepAliveIdleTime() {
502
+ return this.#options.keepAliveIdleTime;
503
+ }
504
+
505
+ /**
506
+ * Sets the keep-alive idle time
507
+ * WIT:
508
+ * ```
509
+ * set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>
510
+ * ```
511
+ *
512
+ * @param {bigint} value - The idle time in nanoseconds
513
+ * @returns {void}
514
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `value` is less than 1
515
+ * @throws {SocketError} for other errors, payload.tag maps the system error
516
+ */
517
+ setKeepAliveIdleTime(value) {
518
+ if (value < 1n) {
519
+ throw new SocketError("invalid-argument");
520
+ }
521
+
522
+ // Ensure that the value passed to the worker never gets rounded down to 0.
523
+ if (value < 1_000_000_000n) {
524
+ value = 1_000_000_000n;
525
+ }
526
+
527
+ if (value === this.#options.keepAliveIdleTime || !this.#options.keepAliveEnabled) {
528
+ this.#options.keepAliveIdleTime = value;
529
+ return;
530
+ }
531
+
532
+ this.#options.keepAliveIdleTime = value;
533
+
534
+ try {
535
+ worker().runSync({
536
+ op: "tcp-set-keep-alive",
537
+ socketId: this.#socketId,
538
+ keepAliveEnabled: this.#options.keepAliveEnabled,
539
+ keepAliveIdleTime: this.#options.keepAliveIdleTime,
540
+ });
541
+ } catch (err) {
542
+ throw SocketError.from(err);
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Gets the keep-alive interval
548
+ * WIT:
549
+ * ```
550
+ * keep-alive-interval: func() -> result<duration, error-code>
551
+ * ```
552
+ *
553
+ * @returns {bigint} The keep-alive interval in nanoseconds
554
+ */
555
+ getKeepAliveInterval() {
556
+ return this.#options.keepAliveInterval;
557
+ }
558
+
559
+ /**
560
+ * Sets the keep-alive interval
561
+ * WIT:
562
+ * ```
563
+ * set-keep-alive-interval: func(value: duration) -> result<_, error-code>
564
+ * ```
565
+ *
566
+ * @param {bigint} value - The interval in nanoseconds
567
+ * @returns {void}
568
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `value` is less than 1
569
+ */
570
+ setKeepAliveInterval(value) {
571
+ if (value < 1n) {
572
+ throw new SocketError("invalid-argument");
573
+ }
574
+ this.#options.keepAliveInterval = value;
575
+ }
576
+
577
+ /**
578
+ * Gets the keep-alive count
579
+ * WIT:
580
+ * ```
581
+ * keep-alive-count: func() -> result<u32, error-code>
582
+ * ```
583
+ *
584
+ * @returns {number} The keep-alive count
585
+ */
586
+ getKeepAliveCount() {
587
+ return this.#options.keepAliveCount;
588
+ }
589
+
590
+ /**
591
+ * Sets the keep-alive count
592
+ * WIT:
593
+ * ```
594
+ * set-keep-alive-count: func(value: u32) -> result<_, error-code>
595
+ * ```
596
+ *
597
+ * @param {number} value - The keep-alive count
598
+ * @returns {void}
599
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `value` is less than 1
600
+ */
601
+ setKeepAliveCount(value) {
602
+ if (value < 1) {
603
+ throw new SocketError("invalid-argument");
604
+ }
605
+ this.#options.keepAliveCount = value;
606
+ }
607
+
608
+ /**
609
+ * Gets the hop limit
610
+ * WIT:
611
+ * ```
612
+ * hop-limit: func() -> result<u8, error-code>
613
+ * ```
614
+ *
615
+ * @returns {number} The hop limit
616
+ */
617
+ getHopLimit() {
618
+ return this.#options.hopLimit;
619
+ }
620
+
621
+ /**
622
+ * Sets the hop limit
623
+ * WIT:
624
+ * ```
625
+ * set-hop-limit: func(value: u8) -> result<_, error-code>
626
+ * ```
627
+ *
628
+ * @param {number} value - The hop limit
629
+ * @returns {void}
630
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `value` is less than 1
631
+ */
632
+ setHopLimit(value) {
633
+ if (value < 1) {
634
+ throw new SocketError("invalid-argument");
635
+ }
636
+ this.#options.hopLimit = value;
637
+ }
638
+
639
+ /**
640
+ * Gets the receive buffer size
641
+ * WIT:
642
+ * ```
643
+ * receive-buffer-size: func() -> result<u64, error-code>
644
+ * ```
645
+ *
646
+ * @returns {bigint} The receive buffer size in bytes
647
+ * @throws {SocketError} for other errors, payload.tag maps the system error
648
+ */
649
+ getReceiveBufferSize() {
650
+ if (this.#options.receiveBufferSize) {
651
+ return this.#options.receiveBufferSize;
652
+ }
653
+
654
+ try {
655
+ const result = worker().runSync({
656
+ op: "tcp-recv-buffer-size",
657
+ socketId: this.#socketId,
658
+ });
659
+
660
+ this.#options.receiveBufferSize = result;
661
+ return result;
662
+ } catch (err) {
663
+ throw SocketError.from(err);
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Sets the receive buffer size
669
+ * WIT:
670
+ * ```
671
+ * set-receive-buffer-size: func(value: u64) -> result<_, error-code>
672
+ * ```
673
+ *
674
+ * @param {bigint} value - The receive buffer size in bytes
675
+ * @returns {void}
676
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `value` is 0
677
+ */
678
+ setReceiveBufferSize(value) {
679
+ if (value <= 0n) {
680
+ throw new SocketError("invalid-argument");
681
+ }
682
+ this.#options.receiveBufferSize = value;
683
+ }
684
+
685
+ /**
686
+ * Gets the send buffer size
687
+ * WIT:
688
+ * ```
689
+ * send-buffer-size: func() -> result<u64, error-code>
690
+ * ```
691
+ *
692
+ * @returns {bigint} The send buffer size in bytes
693
+ * @throws {SocketError} for other errors, payload.tag maps the system error
694
+ */
695
+ getSendBufferSize() {
696
+ if (this.#options.sendBufferSize) {
697
+ return this.#options.sendBufferSize;
698
+ }
699
+
700
+ try {
701
+ const result = worker().runSync({
702
+ op: "tcp-send-buffer-size",
703
+ socketId: this.#socketId,
704
+ });
705
+
706
+ this.#options.sendBufferSize = result;
707
+ return result;
708
+ } catch (err) {
709
+ throw SocketError.from(err);
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Sets the send buffer size
715
+ * WIT:
716
+ * ```
717
+ * set-send-buffer-size: func(value: u64) -> result<_, error-code>
718
+ * ```
719
+ *
720
+ * @param {bigint} value - The send buffer size in bytes
721
+ * @returns {void}
722
+ * @throws {SocketError} with payload.tag 'invalid-argument' if `value` is 0
723
+ */
724
+ setSendBufferSize(value) {
725
+ if (value <= 0n) {
726
+ throw new SocketError("invalid-argument");
727
+ }
728
+ this.#options.sendBufferSize = value;
729
+ }
730
+
731
+ /**
732
+ * Disposes the socket
733
+ */
734
+ [Symbol.dispose]() {
735
+ if (this.#socketId) {
736
+ worker().runSync({
737
+ op: "tcp-dispose",
738
+ socketId: this.#socketId,
739
+ });
740
+ }
741
+
742
+ this.#socketId = null;
743
+ this.#state = STATE.CLOSED;
744
+ }
745
+
746
+ /**
747
+ * Validates if a local address is valid for this socket
748
+ * @param {Object} localAddress - The local address to validate
749
+ * @returns {boolean} True if the address is valid, false otherwise
750
+ * @private
751
+ */
752
+ #isValidLocalAddress(localAddress) {
753
+ return (
754
+ this.#family === localAddress.tag &&
755
+ (isUnicastIpAddress(localAddress) || isWildcardIpAddress(localAddress)) &&
756
+ !isIpv4MappedAddress(localAddress)
757
+ );
758
+ }
759
+
760
+ /**
761
+ * Validates if a remote address is valid for this socket
762
+ * @param {Object} remoteAddress - The remote address to validate
763
+ * @returns {boolean} True if the address is valid, false otherwise
764
+ * @private
765
+ */
766
+ #isValidRemoteAddress(remoteAddress) {
767
+ return (
768
+ this.#family === remoteAddress.tag &&
769
+ remoteAddress.val.port !== 0 &&
770
+ isUnicastIpAddress(remoteAddress) &&
771
+ !isWildcardIpAddress(remoteAddress) &&
772
+ !isMulticastIpAddress(remoteAddress) &&
773
+ !isIpv4MappedAddress(remoteAddress)
774
+ );
775
+ }
776
+ }