@bytecodealliance/preview2-shim 0.14.1 → 0.15.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.
@@ -2,8 +2,10 @@ import { fileURLToPath } from "node:url";
2
2
  import { createSyncFn } from "../synckit/index.js";
3
3
  import {
4
4
  CALL_MASK,
5
- CALL_SHIFT,
6
5
  CALL_TYPE_MASK,
6
+ FILE,
7
+ HTTP_SERVER_INCOMING_HANDLER,
8
+ HTTP,
7
9
  INPUT_STREAM_BLOCKING_READ,
8
10
  INPUT_STREAM_BLOCKING_SKIP,
9
11
  INPUT_STREAM_DISPOSE,
@@ -23,28 +25,62 @@ import {
23
25
  OUTPUT_STREAM_WRITE,
24
26
  POLL_POLL_LIST,
25
27
  POLL_POLLABLE_BLOCK,
28
+ POLL_POLLABLE_DISPOSE,
26
29
  POLL_POLLABLE_READY,
30
+ SOCKET_TCP,
31
+ STDERR,
32
+ STDIN,
33
+ STDOUT,
34
+ reverseMap,
27
35
  } from "./calls.js";
28
- import { STDERR } from "./calls.js";
29
-
30
- const DEBUG = false;
36
+ import { _rawDebug, exit, stderr, stdout, env } from "node:process";
31
37
 
32
38
  const workerPath = fileURLToPath(
33
39
  new URL("./worker-thread.js", import.meta.url)
34
40
  );
35
41
 
42
+ const httpIncomingHandlers = new Map();
43
+ export function registerIncomingHttpHandler(id, handler) {
44
+ httpIncomingHandlers.set(id, handler);
45
+ }
46
+
47
+ const instanceId = Math.round(Math.random() * 1000).toString();
48
+ const DEBUG_DEFAULT = false;
49
+ const DEBUG =
50
+ env.PREVIEW2_SHIM_DEBUG === "0"
51
+ ? false
52
+ : env.PREVIEW2_SHIM_DEBUG === "1"
53
+ ? true
54
+ : DEBUG_DEFAULT;
55
+
36
56
  /**
37
57
  * @type {(call: number, id: number | null, payload: any) -> any}
38
58
  */
39
- export let ioCall = createSyncFn(workerPath);
59
+ export let ioCall = createSyncFn(workerPath, DEBUG, (type, id, payload) => {
60
+ // 'callbacks' from the worker
61
+ // ONLY happens for an http server incoming handler, and NOTHING else (not even sockets, since accept is sync!)
62
+ if (type !== HTTP_SERVER_INCOMING_HANDLER)
63
+ throw new Error(
64
+ "Internal error: only incoming handler callback is permitted"
65
+ );
66
+ const handler = httpIncomingHandlers.get(id);
67
+ if (!handler)
68
+ throw new Error(
69
+ `Internal error: no incoming handler registered for server ${id}`
70
+ );
71
+ handler(payload);
72
+ });
40
73
  if (DEBUG) {
41
74
  const _ioCall = ioCall;
42
75
  ioCall = function ioCall(num, id, payload) {
76
+ if (typeof id !== "number" && id !== null)
77
+ throw new Error("id must be a number or null");
43
78
  let ret;
44
79
  try {
45
- process._rawDebug(
46
- (num & CALL_MASK) >> CALL_SHIFT,
47
- num & CALL_TYPE_MASK,
80
+ _rawDebug(
81
+ instanceId,
82
+ reverseMap[num & CALL_MASK],
83
+ reverseMap[num & CALL_TYPE_MASK],
48
84
  id,
49
85
  payload
50
86
  );
@@ -54,15 +90,56 @@ if (DEBUG) {
54
90
  ret = e;
55
91
  throw ret;
56
92
  } finally {
57
- process._rawDebug("->", ret);
93
+ _rawDebug(instanceId, "->", ret);
58
94
  }
59
95
  };
60
96
  }
61
97
 
62
98
  const symbolDispose = Symbol.dispose || Symbol.for("dispose");
63
99
 
100
+ const finalizationRegistry = new FinalizationRegistry(
101
+ (dispose) => void dispose()
102
+ );
103
+
104
+ const dummySymbol = Symbol();
105
+
106
+ /**
107
+ *
108
+ * @param {any} resource
109
+ * @param {any} parentResource
110
+ * @param {number} id
111
+ * @param {(number) => void} disposeFn
112
+ */
113
+ export function registerDispose(resource, parentResource, id, disposeFn) {
114
+ // While strictly speaking all components should handle their disposal,
115
+ // this acts as a last-resort to catch all missed drops through the JS GC.
116
+ // Mainly for two cases - (1) components which are long lived, that get shut
117
+ // down and (2) users that interface with low-level WASI APIs directly in JS
118
+ // for various reasons may end up leaning on JS GC inadvertantly.
119
+ function finalizer() {
120
+ // This has no functional purpose other than to pin a strong reference
121
+ // from the child resource's finalizer to the parent resource, to ensure
122
+ // that we can never finalize a parent resource before a child resource.
123
+ // This makes the generational JS GC become piecewise over child resource
124
+ // graphs (generational at each resource hierarchy level at least).
125
+ if (parentResource?.[dummySymbol]) return;
126
+ disposeFn(id);
127
+ }
128
+ finalizationRegistry.register(resource, finalizer, finalizer);
129
+ return finalizer;
130
+ }
131
+
132
+ export function earlyDispose(finalizer) {
133
+ finalizationRegistry.unregister(finalizer);
134
+ finalizer();
135
+ }
136
+
64
137
  const _Error = Error;
65
138
  const IoError = class Error extends _Error {
139
+ constructor(payload) {
140
+ super(payload);
141
+ this.payload = payload;
142
+ }
66
143
  toDebugString() {
67
144
  return this.message;
68
145
  }
@@ -72,24 +149,21 @@ function streamIoErrorCall(call, id, payload) {
72
149
  try {
73
150
  return ioCall(call, id, payload);
74
151
  } catch (e) {
75
- if (e.tag === 'closed')
76
- throw e;
152
+ if (e.tag === "closed") throw e;
77
153
  if (e.tag === "last-operation-failed") {
78
- e.val = new IoError(e.val);
154
+ e.val = new IoError(Object.assign(new Error(e.val.message), e.val));
79
155
  throw e;
80
156
  }
81
157
  // any invalid error is a trap
82
158
  console.trace(e);
83
- process.exit(1);
159
+ exit(1);
84
160
  }
85
161
  }
86
162
 
87
163
  class InputStream {
88
164
  #id;
89
165
  #streamType;
90
- get _id() {
91
- return this.#id;
92
- }
166
+ #finalizer;
93
167
  read(len) {
94
168
  return streamIoErrorCall(
95
169
  INPUT_STREAM_READ | this.#streamType,
@@ -120,24 +194,65 @@ class InputStream {
120
194
  }
121
195
  subscribe() {
122
196
  return pollableCreate(
123
- ioCall(INPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id)
197
+ ioCall(INPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id),
198
+ this
124
199
  );
125
200
  }
126
- [symbolDispose]() {
127
- ioCall(INPUT_STREAM_DISPOSE | this.#streamType, this.#id);
128
- }
129
201
  static _id(stream) {
130
202
  return stream.#id;
131
203
  }
132
204
  /**
133
- * @param {InputStreamType} streamType
205
+ * @param {FILE | SOCKET_TCP | STDIN | HTTP} streamType
134
206
  */
135
207
  static _create(streamType, id) {
136
208
  const stream = new InputStream();
137
209
  stream.#id = id;
138
210
  stream.#streamType = streamType;
211
+ let disposeFn;
212
+ switch (streamType) {
213
+ case FILE:
214
+ disposeFn = fileInputStreamDispose;
215
+ break;
216
+ case SOCKET_TCP:
217
+ disposeFn = socketTcpInputStreamDispose;
218
+ break;
219
+ case STDIN:
220
+ disposeFn = stdinInputStreamDispose;
221
+ break;
222
+ case HTTP:
223
+ disposeFn = httpInputStreamDispose;
224
+ break;
225
+ default:
226
+ throw new Error(
227
+ "wasi-io trap: Dispose function not created for stream type " +
228
+ reverseMap[streamType]
229
+ );
230
+ }
231
+ stream.#finalizer = registerDispose(stream, null, id, disposeFn);
139
232
  return stream;
140
233
  }
234
+ [symbolDispose]() {
235
+ if (this.#finalizer) {
236
+ earlyDispose(this.#finalizer);
237
+ this.#finalizer = null;
238
+ }
239
+ }
240
+ }
241
+
242
+ function fileInputStreamDispose(id) {
243
+ ioCall(INPUT_STREAM_DISPOSE | FILE, id, null);
244
+ }
245
+
246
+ function socketTcpInputStreamDispose(id) {
247
+ ioCall(INPUT_STREAM_DISPOSE | SOCKET_TCP, id, null);
248
+ }
249
+
250
+ function stdinInputStreamDispose(id) {
251
+ ioCall(INPUT_STREAM_DISPOSE | STDIN, id, null);
252
+ }
253
+
254
+ function httpInputStreamDispose(id) {
255
+ ioCall(INPUT_STREAM_DISPOSE | HTTP, id, null);
141
256
  }
142
257
 
143
258
  export const inputStreamCreate = InputStream._create;
@@ -149,9 +264,7 @@ delete InputStream._id;
149
264
  class OutputStream {
150
265
  #id;
151
266
  #streamType;
152
- get _id() {
153
- return this.#id;
154
- }
267
+ #finalizer;
155
268
  checkWrite(len) {
156
269
  return streamIoErrorCall(
157
270
  OUTPUT_STREAM_CHECK_WRITE | this.#streamType,
@@ -169,8 +282,7 @@ class OutputStream {
169
282
  }
170
283
  blockingWriteAndFlush(buf) {
171
284
  if (this.#streamType <= STDERR) {
172
- const stream =
173
- this.#streamType === STDERR ? process.stderr : process.stdout;
285
+ const stream = this.#streamType === STDERR ? stderr : stdout;
174
286
  return void stream.write(buf);
175
287
  }
176
288
  return streamIoErrorCall(
@@ -206,16 +318,14 @@ class OutputStream {
206
318
  return streamIoErrorCall(
207
319
  OUTPUT_STREAM_SPLICE | this.#streamType,
208
320
  this.#id,
209
- src.#id,
210
- len
321
+ { src: src.#id, len }
211
322
  );
212
323
  }
213
324
  blockingSplice(src, len) {
214
325
  return streamIoErrorCall(
215
326
  OUTPUT_STREAM_BLOCKING_SPLICE | this.#streamType,
216
327
  this.#id,
217
- src.#id,
218
- len
328
+ { src: inputStreamId(src), len }
219
329
  );
220
330
  }
221
331
  subscribe() {
@@ -223,9 +333,6 @@ class OutputStream {
223
333
  ioCall(OUTPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id)
224
334
  );
225
335
  }
226
- [symbolDispose]() {
227
- ioCall(OUTPUT_STREAM_DISPOSE | this.#streamType, this.#id);
228
- }
229
336
 
230
337
  static _id(outputStream) {
231
338
  return outputStream.#id;
@@ -238,8 +345,54 @@ class OutputStream {
238
345
  const stream = new OutputStream();
239
346
  stream.#id = id;
240
347
  stream.#streamType = streamType;
348
+ let disposeFn;
349
+ switch (streamType) {
350
+ case STDOUT:
351
+ disposeFn = stdoutOutputStreamDispose;
352
+ break;
353
+ case STDERR:
354
+ disposeFn = stderrOutputStreamDispose;
355
+ break;
356
+ case SOCKET_TCP:
357
+ disposeFn = socketTcpOutputStreamDispose;
358
+ break;
359
+ case FILE:
360
+ disposeFn = fileOutputStreamDispose;
361
+ break;
362
+ case HTTP:
363
+ return stream;
364
+ default:
365
+ throw new Error(
366
+ "wasi-io trap: Dispose function not created for stream type " +
367
+ reverseMap[streamType]
368
+ );
369
+ }
370
+ stream.#finalizer = registerDispose(stream, null, id, disposeFn);
241
371
  return stream;
242
372
  }
373
+
374
+ [symbolDispose]() {
375
+ if (this.#finalizer) {
376
+ earlyDispose(this.#finalizer);
377
+ this.#finalizer = null;
378
+ }
379
+ }
380
+ }
381
+
382
+ function stdoutOutputStreamDispose(id) {
383
+ ioCall(OUTPUT_STREAM_DISPOSE | STDOUT, id);
384
+ }
385
+
386
+ function stderrOutputStreamDispose(id) {
387
+ ioCall(OUTPUT_STREAM_DISPOSE | STDERR, id);
388
+ }
389
+
390
+ function socketTcpOutputStreamDispose(id) {
391
+ ioCall(OUTPUT_STREAM_DISPOSE | SOCKET_TCP, id);
392
+ }
393
+
394
+ function fileOutputStreamDispose(id) {
395
+ ioCall(OUTPUT_STREAM_DISPOSE | FILE, id);
243
396
  }
244
397
 
245
398
  export const outputStreamCreate = OutputStream._create;
@@ -252,71 +405,57 @@ export const error = { Error: IoError };
252
405
 
253
406
  export const streams = { InputStream, OutputStream };
254
407
 
408
+ function pollableDispose(id) {
409
+ ioCall(POLL_POLLABLE_DISPOSE, id);
410
+ }
411
+
255
412
  class Pollable {
256
413
  #id;
257
- #ready = false;
258
- get _id() {
259
- return this.#id;
260
- }
414
+ #finalizer;
261
415
  ready() {
262
- if (this.#ready) return true;
263
- const ready = ioCall(POLL_POLLABLE_READY, this.#id);
264
- if (ready) this.#ready = true;
265
- return ready;
416
+ if (this.#id === 0) return true;
417
+ return ioCall(POLL_POLLABLE_READY, this.#id);
266
418
  }
267
419
  block() {
268
- if (!this.#ready) {
420
+ if (this.#id !== 0) {
269
421
  ioCall(POLL_POLLABLE_BLOCK, this.#id);
270
- this.#ready = true;
271
422
  }
272
423
  }
273
424
  static _getId(pollable) {
274
425
  return pollable.#id;
275
426
  }
276
- static _create(id) {
427
+ static _create(id, parent) {
277
428
  const pollable = new Pollable();
278
429
  pollable.#id = id;
279
- if (id === 0) pollable.#ready = true;
430
+ pollable.#finalizer = registerDispose(
431
+ pollable,
432
+ parent,
433
+ id,
434
+ pollableDispose
435
+ );
280
436
  return pollable;
281
437
  }
282
- static _listToIds(list) {
283
- return list.map((pollable) => pollable.#id);
284
- }
285
- static _markReady(pollable) {
286
- pollable.#ready = true;
438
+ [symbolDispose]() {
439
+ if (this.#finalizer) {
440
+ earlyDispose(this.#finalizer);
441
+ this.#finalizer = null;
442
+ }
287
443
  }
288
444
  }
289
445
 
290
446
  export const pollableCreate = Pollable._create;
291
447
  delete Pollable._create;
292
448
 
293
- const pollableListToIds = Pollable._listToIds;
294
- delete Pollable._listToIds;
295
-
296
- const pollableMarkReady = Pollable._markReady;
297
- delete Pollable._markReady;
298
-
299
449
  const pollableGetId = Pollable._getId;
300
450
  delete Pollable._getId;
301
451
 
302
452
  export const poll = {
303
453
  Pollable,
304
454
  poll(list) {
305
- const includeList = ioCall(POLL_POLL_LIST, null, pollableListToIds(list));
306
- return list.filter((pollable) => {
307
- if (includeList.includes(pollableGetId(pollable))) {
308
- pollableMarkReady(pollable);
309
- return true;
310
- }
311
- return false;
312
- });
455
+ return ioCall(POLL_POLL_LIST, null, list.map(pollableGetId));
313
456
  },
314
457
  };
315
458
 
316
- export function resolvedPoll() {
317
- return pollableCreate(0);
318
- }
319
-
320
459
  export function createPoll(call, id, initPayload) {
321
460
  return pollableCreate(ioCall(call, id, initPayload));
322
461
  }
@@ -0,0 +1,285 @@
1
+ import {
2
+ createFuture,
3
+ createReadableStream,
4
+ createReadableStreamPollState,
5
+ createWritableStream,
6
+ futureDispose,
7
+ futureTakeValue,
8
+ pollStateReady,
9
+ verifyPollsDroppedForDrop,
10
+ } from "./worker-thread.js";
11
+ const { TCP, constants: TCPConstants } = process.binding("tcp_wrap");
12
+ import {
13
+ convertSocketError,
14
+ convertSocketErrorCode,
15
+ ipSocketAddress,
16
+ isIPv4MappedAddress,
17
+ isMulticastIpAddress,
18
+ isUnicastIpAddress,
19
+ isWildcardAddress,
20
+ noLookup,
21
+ serializeIpAddress,
22
+ SOCKET_STATE_BIND,
23
+ SOCKET_STATE_BOUND,
24
+ SOCKET_STATE_CLOSED,
25
+ SOCKET_STATE_CONNECT,
26
+ SOCKET_STATE_CONNECTION,
27
+ SOCKET_STATE_INIT,
28
+ SOCKET_STATE_LISTEN,
29
+ SOCKET_STATE_LISTENER,
30
+ } from "./worker-sockets.js";
31
+ import { Socket, Server } from "node:net";
32
+
33
+ /**
34
+ * @typedef {import("../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress
35
+ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily
36
+ * @typedef {import("node:net").Socket} TcpSocket
37
+ *
38
+ * @typedef {{
39
+ * tcpSocket: number,
40
+ * err: Error | null,
41
+ * pollState: PollState,
42
+ * }} PendingAccept
43
+ *
44
+ * @typedef {{
45
+ * state: number,
46
+ * future: number | null,
47
+ * socket: TcpSocket | null,
48
+ * listenBacklogSize: number,
49
+ * handle: TCP,
50
+ * pendingAccepts: PendingAccept[],
51
+ * pollState: PollState,
52
+ * }} TcpSocketRecord
53
+ */
54
+
55
+ /**
56
+ * @type {Map<number, TcpSocketRecord>}
57
+ */
58
+ export const tcpSockets = new Map();
59
+
60
+ let tcpSocketCnt = 0;
61
+
62
+ /**
63
+ * @param {IpAddressFamily} addressFamily
64
+ */
65
+ export function createTcpSocket() {
66
+ const handle = new TCP(TCPConstants.SOCKET);
67
+ tcpSockets.set(++tcpSocketCnt, {
68
+ state: SOCKET_STATE_INIT,
69
+ future: null,
70
+ listenBacklogSize: 128,
71
+ handle,
72
+ pendingAccepts: [],
73
+ pollState: { ready: true, listener: null, polls: [], parentStream: null },
74
+ });
75
+ return tcpSocketCnt;
76
+ }
77
+
78
+ export function socketTcpFinish(id, fromState, toState) {
79
+ const socket = tcpSockets.get(id);
80
+ if (socket.state !== fromState) throw "not-in-progress";
81
+ if (!socket.pollState.ready) throw "would-block";
82
+ const { tag, val } = futureTakeValue(socket.future).val;
83
+ futureDispose(socket.future, false);
84
+ socket.future = null;
85
+ if (tag === "err") {
86
+ socket.state = SOCKET_STATE_CLOSED;
87
+ throw val;
88
+ } else {
89
+ socket.state = toState;
90
+ // for the listener, we must immediately transition back to unresolved
91
+ if (toState === SOCKET_STATE_LISTENER) socket.pollState.ready = false;
92
+ return val;
93
+ }
94
+ }
95
+
96
+ export function socketTcpBindStart(id, localAddress, family) {
97
+ const socket = tcpSockets.get(id);
98
+ if (socket.state !== SOCKET_STATE_INIT) throw "invalid-state";
99
+ if (
100
+ family !== localAddress.tag ||
101
+ !isUnicastIpAddress(localAddress) ||
102
+ isIPv4MappedAddress(localAddress)
103
+ )
104
+ throw "invalid-argument";
105
+ socket.state = SOCKET_STATE_BIND;
106
+ const { handle } = socket;
107
+ socket.future = createFuture(
108
+ (async () => {
109
+ const address = serializeIpAddress(localAddress);
110
+ const port = localAddress.val.port;
111
+ const code =
112
+ localAddress.tag === "ipv6"
113
+ ? handle.bind6(address, port, TCPConstants.UV_TCP_IPV6ONLY)
114
+ : handle.bind(address, port);
115
+ if (code !== 0) throw convertSocketErrorCode(-code);
116
+ // This is a Node.js / libuv quirk to force the bind error to be thrown
117
+ // (specifically address-in-use).
118
+ {
119
+ const out = {};
120
+ const code = handle.getsockname(out);
121
+ if (code !== 0) throw convertSocketErrorCode(-code);
122
+ }
123
+ })(),
124
+ socket.pollState
125
+ );
126
+ }
127
+
128
+ export function socketTcpConnectStart(id, remoteAddress, family) {
129
+ const socket = tcpSockets.get(id);
130
+ if (socket.state !== SOCKET_STATE_INIT && socket.state !== SOCKET_STATE_BOUND)
131
+ throw "invalid-state";
132
+ if (
133
+ isWildcardAddress(remoteAddress) ||
134
+ family !== remoteAddress.tag ||
135
+ !isUnicastIpAddress(remoteAddress) ||
136
+ isMulticastIpAddress(remoteAddress) ||
137
+ remoteAddress.val.port === 0 ||
138
+ isIPv4MappedAddress(remoteAddress)
139
+ ) {
140
+ throw "invalid-argument";
141
+ }
142
+ socket.state = SOCKET_STATE_CONNECT;
143
+ socket.future = createFuture(
144
+ new Promise((resolve, reject) => {
145
+ const tcpSocket = (socket.tcpSocket = new Socket({
146
+ handle: socket.handle,
147
+ pauseOnCreate: true,
148
+ allowHalfOpen: true,
149
+ }));
150
+ function handleErr(err) {
151
+ tcpSocket.off("connect", handleConnect);
152
+ reject(convertSocketError(err));
153
+ }
154
+ function handleConnect() {
155
+ tcpSocket.off("error", handleErr);
156
+ resolve([
157
+ createReadableStream(tcpSocket),
158
+ createWritableStream(tcpSocket),
159
+ ]);
160
+ }
161
+ tcpSocket.once("connect", handleConnect);
162
+ tcpSocket.once("error", handleErr);
163
+ tcpSocket.connect({
164
+ port: remoteAddress.val.port,
165
+ host: serializeIpAddress(remoteAddress),
166
+ lookup: noLookup,
167
+ });
168
+ }),
169
+ socket.pollState
170
+ );
171
+ }
172
+
173
+ export function socketTcpListenStart(id) {
174
+ const socket = tcpSockets.get(id);
175
+ if (socket.state !== SOCKET_STATE_BOUND) throw "invalid-state";
176
+ const { handle } = socket;
177
+ socket.state = SOCKET_STATE_LISTEN;
178
+ socket.future = createFuture(
179
+ new Promise((resolve, reject) => {
180
+ const server = new Server({ pauseOnConnect: true, allowHalfOpen: true });
181
+ function handleErr(err) {
182
+ server.off("listening", handleListen);
183
+ reject(convertSocketError(err));
184
+ }
185
+ function handleListen() {
186
+ server.off("error", handleErr);
187
+ server.on("connection", (tcpSocket) => {
188
+ pollStateReady(socket.pollState);
189
+ const pollState = createReadableStreamPollState(tcpSocket);
190
+ socket.pendingAccepts.push({ tcpSocket, err: null, pollState });
191
+ });
192
+ server.on("error", (err) => {
193
+ pollStateReady(socket.pollState);
194
+ socket.pendingAccepts.push({ tcpSocket: null, err, pollState: null });
195
+ });
196
+ resolve();
197
+ }
198
+ server.once("listening", handleListen);
199
+ server.once("error", handleErr);
200
+ server.listen(handle, socket.listenBacklogSize);
201
+ }),
202
+ socket.pollState
203
+ );
204
+ }
205
+
206
+ export function socketTcpAccept(id) {
207
+ const socket = tcpSockets.get(id);
208
+ if (socket.state !== SOCKET_STATE_LISTENER) throw "invalid-state";
209
+ if (socket.pendingAccepts.length === 0) throw "would-block";
210
+ const accept = socket.pendingAccepts.shift();
211
+ if (accept.err) {
212
+ socket.state = SOCKET_STATE_CLOSED;
213
+ throw convertSocketError(accept.err);
214
+ }
215
+ if (socket.pendingAccepts.length === 0) socket.pollState.ready = false;
216
+ tcpSockets.set(++tcpSocketCnt, {
217
+ state: SOCKET_STATE_CONNECTION,
218
+ future: null,
219
+ listenBacklogSize: 128,
220
+ handle: accept.tcpSocket._handle,
221
+ pendingAccepts: [],
222
+ pollState: accept.pollState,
223
+ });
224
+ return [
225
+ tcpSocketCnt,
226
+ createReadableStream(accept.tcpSocket, accept.pollState),
227
+ createWritableStream(accept.tcpSocket),
228
+ ];
229
+ }
230
+
231
+ export function socketTcpSetListenBacklogSize(id, backlogSize) {
232
+ const socket = tcpSockets.get(id);
233
+ if (
234
+ socket.state === SOCKET_STATE_LISTEN ||
235
+ socket.state === SOCKET_STATE_LISTENER
236
+ )
237
+ throw "not-supported";
238
+ if (
239
+ socket.state !== SOCKET_STATE_INIT &&
240
+ socket.state !== SOCKET_STATE_BIND &&
241
+ socket.state !== SOCKET_STATE_BOUND
242
+ )
243
+ throw "invalid-state";
244
+ socket.listenBacklogSize = Number(backlogSize);
245
+ }
246
+
247
+ export function socketTcpGetLocalAddress(id) {
248
+ const { handle } = tcpSockets.get(id);
249
+ const out = {};
250
+ const code = handle.getsockname(out);
251
+ if (code !== 0) throw convertSocketErrorCode(-code);
252
+ return ipSocketAddress(out.family.toLowerCase(), out.address, out.port);
253
+ }
254
+
255
+ export function socketTcpGetRemoteAddress(id) {
256
+ const { handle } = tcpSockets.get(id);
257
+ const out = {};
258
+ const code = handle.getpeername(out);
259
+ if (code !== 0) throw convertSocketErrorCode(-code);
260
+ return ipSocketAddress(out.family.toLowerCase(), out.address, out.port);
261
+ }
262
+
263
+ export function socketTcpShutdown(id, shutdownType) {
264
+ const socket = tcpSockets.get(id);
265
+ if (socket.state !== SOCKET_STATE_CONNECTION) throw "invalid-state";
266
+ // Node.js only supports a write shutdown, which is triggered on end
267
+ if (shutdownType === "send" || shutdownType === "both")
268
+ socket.tcpSocket.end();
269
+ }
270
+
271
+ export function socketTcpSetKeepAlive(id, { keepAlive, keepAliveIdleTime }) {
272
+ const { handle } = tcpSockets.get(id);
273
+ const code = handle.setKeepAlive(
274
+ keepAlive,
275
+ Number(keepAliveIdleTime / 1_000_000_000n)
276
+ );
277
+ if (code !== 0) throw convertSocketErrorCode(-code);
278
+ }
279
+
280
+ export function socketTcpDispose(id) {
281
+ const socket = tcpSockets.get(id);
282
+ verifyPollsDroppedForDrop(socket.pollState, "tcp socket");
283
+ socket.handle.close();
284
+ tcpSockets.delete(id);
285
+ }