@bytecodealliance/preview3-shim 0.3.2 → 0.5.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.
@@ -60,7 +60,7 @@ declare class Descriptor {
60
60
  * @returns {Promise<void>}
61
61
  * @throws {FSError} `payload.tag` contains mapped WASI error code.
62
62
  */
63
- advise(_offset: any, _length: any, _advice: any): Promise<void>;
63
+ advise(offset: any, length: any, advice: any): Promise<void>;
64
64
  /**
65
65
  * Synchronize file data to disk.
66
66
  * WIT:
@@ -158,11 +158,14 @@ class Descriptor {
158
158
  * @returns {Promise<void>}
159
159
  * @throws {FSError} `payload.tag` contains mapped WASI error code.
160
160
  */
161
- async advise(_offset, _length, _advice) {
162
- // TODO: This not directly suported on Node.js:
163
- // https://github.com/bytecodealliance/jco/issues/718
164
- if ((await this.getType()).tag === "directory") {
165
- throw new FSError("bad-descriptor");
161
+ async advise(offset, length, advice) {
162
+ this.#ensureHandle();
163
+ try {
164
+ const { fadvise } = await import("@bytecodealliance/jco-node-fs");
165
+ fadvise(this.#handle.fd, offset, length, advice);
166
+ }
167
+ catch (error) {
168
+ throw FSError.from(error);
166
169
  }
167
170
  }
168
171
  /**
@@ -255,7 +255,13 @@ export class TcpSocket {
255
255
  }
256
256
  let stream;
257
257
  try {
258
- stream = readableByteStreamFromReader(data, { name: "tcp send data" });
258
+ // A remote error arrives from the TCP worker through a MessagePort. Yield
259
+ // after each chunk so a guest producer that completes writes immediately
260
+ // cannot starve that error (and its stream cancellation) indefinitely.
261
+ stream = readableByteStreamFromReader(data, {
262
+ name: "tcp send data",
263
+ yieldAfterChunk: true,
264
+ });
259
265
  }
260
266
  catch (error) {
261
267
  if (error instanceof TypeError) {
@@ -1,5 +1,10 @@
1
1
  export declare const DEFAULT_BYTE_STREAM_CHUNK_SIZE: number;
2
2
  declare const symbolDispose: symbol;
3
+ interface ReadableByteStreamOptions {
4
+ chunkSize?: number;
5
+ name?: string;
6
+ yieldAfterChunk?: boolean;
7
+ }
3
8
  /**
4
9
  * Creates a transferable ReadableStream from an async iterator.
5
10
  *
@@ -22,9 +27,10 @@ export declare function readableStreamFromIterator(iterator: any, name?: string)
22
27
  * @param {object} [opts={}] - Optional adapter settings.
23
28
  * @param {number} [opts.chunkSize=DEFAULT_BYTE_STREAM_CHUNK_SIZE] - Maximum bytes to request per generated `read()` call.
24
29
  * @param {string} [opts.name="stream reader"] - Reader name used in error messages.
30
+ * @param {boolean} [opts.yieldAfterChunk=false] - Yield one event-loop turn after each chunk.
25
31
  * @returns {ReadableStream<Uint8Array>} A transferable stream of byte chunks.
26
32
  */
27
- export declare function readableByteStreamFromReader(reader: any, opts?: {}): ReadableStream<any>;
33
+ export declare function readableByteStreamFromReader(reader: any, opts?: ReadableByteStreamOptions): ReadableStream<any>;
28
34
  export declare function _byteChunk(value: any): any;
29
35
  /**
30
36
  * Creates a bidirectional stream with separate reader and writer interfaces.
@@ -46,6 +46,7 @@ export function readableStreamFromIterator(iterator, name = "iterator") {
46
46
  * @param {object} [opts={}] - Optional adapter settings.
47
47
  * @param {number} [opts.chunkSize=DEFAULT_BYTE_STREAM_CHUNK_SIZE] - Maximum bytes to request per generated `read()` call.
48
48
  * @param {string} [opts.name="stream reader"] - Reader name used in error messages.
49
+ * @param {boolean} [opts.yieldAfterChunk=false] - Yield one event-loop turn after each chunk.
49
50
  * @returns {ReadableStream<Uint8Array>} A transferable stream of byte chunks.
50
51
  */
51
52
  export function readableByteStreamFromReader(reader, opts = {}) {
@@ -58,6 +59,9 @@ export function readableByteStreamFromReader(reader, opts = {}) {
58
59
  return;
59
60
  }
60
61
  controller.enqueue(_byteChunk(value));
62
+ if (opts.yieldAfterChunk) {
63
+ await new Promise((resolve) => setTimeout(resolve, 0));
64
+ }
61
65
  },
62
66
  cancel(reason) {
63
67
  return source.cancel?.(reason);
@@ -1,9 +1,6 @@
1
1
  import { Server, Socket } from "node:net";
2
- import { Readable } from "stream";
3
- import { pipeline } from "stream/promises";
4
2
  import { once } from "node:events";
5
3
  import { isMainThread, parentPort } from "worker_threads";
6
- import process from "node:process";
7
4
  //#region dist/nodejs/workers/resource-worker.js
8
5
  function Router() {
9
6
  const _hooks = {
@@ -255,7 +252,6 @@ var SocketError = class SocketError extends Error {
255
252
  };
256
253
  //#endregion
257
254
  //#region dist/nodejs/workers/tcp-worker.js
258
- const { TCP, constants: TCPConstants } = process.binding("tcp_wrap");
259
255
  const sockets = /* @__PURE__ */ new Map();
260
256
  let NEXT_SOCKET_ID = 0n;
261
257
  Router().beforeAll((msg) => {
@@ -263,14 +259,15 @@ Router().beforeAll((msg) => {
263
259
  }).op("tcp-create", handleTcpCreate).op("tcp-bind", handleTcpBind).op("tcp-connect", handleTcpConnect).op("tcp-listen", handleTcpListen).op("tcp-send", handleTcpSend).op("tcp-receive", handleTcpReceive).op("tcp-get-local-address", handleGetLocalAddress).op("tcp-get-remote-address", handleGetRemoteAddress).op("tcp-set-listen-backlog-size", handleTcpSetBacklogSize).op("tcp-set-keep-alive", handleTcpSetKeepAlive).op("tcp-recv-buffer-size", handleRecvBufferSize).op("tcp-send-buffer-size", handleSendBufferSize).op("tcp-dispose", handleTcpDispose);
264
260
  function handleTcpCreate({ family }) {
265
261
  const socketId = NEXT_SOCKET_ID++;
266
- const handle = new TCP(TCPConstants.SOCKET);
267
262
  sockets.set(socketId, {
268
- handle,
269
263
  family,
270
264
  tcp: null,
271
265
  server: null,
266
+ acceptWriter: null,
272
267
  backlog: 128,
273
268
  localAddress: null,
269
+ keepAliveEnabled: false,
270
+ keepAliveIdleTime: 0,
274
271
  disposed: false,
275
272
  activeStreams: 0
276
273
  });
@@ -280,83 +277,132 @@ async function handleTcpBind({ socketId, localAddress }) {
280
277
  const socket = sockets.get(socketId);
281
278
  const address = serializeIpAddress(localAddress);
282
279
  const port = localAddress.val.port;
283
- const { handle, family } = socket;
280
+ const { family } = socket;
284
281
  if ([...sockets].some(([id, { localAddress: boundAddress }]) => id !== socketId && boundAddress && ipAddressConflict(boundAddress, localAddress))) {
285
282
  const err = /* @__PURE__ */ new Error("EADDRINUSE");
286
283
  err.code = "EADDRINUSE";
287
284
  throw err;
288
285
  }
289
- await new Promise((resolve, reject) => {
290
- let code;
291
- if (family === "ipv6") code = handle.bind6(address, port, TCPConstants.UV_TCP_IPV6ONLY);
292
- else code = handle.bind(address, port);
293
- if (code !== 0) reject(SocketError.from(-code));
294
- else resolve();
286
+ const server = socket.server = createTcpServer(socket);
287
+ const onListening = once(server, "listening");
288
+ server.listen({
289
+ host: address,
290
+ port,
291
+ backlog: socket.backlog,
292
+ ipv6Only: family === "ipv6"
295
293
  });
296
- const out = {};
297
- const code = handle.getsockname(out);
298
- if (code !== 0) throw SocketError.from(-code);
299
- socket.localAddress = makeIpAddress(out.family.toLowerCase(), out.address, out.port);
294
+ await onListening;
295
+ const boundAddress = server.address();
296
+ if (!boundAddress || typeof boundAddress === "string") throw new SocketError("invalid-state");
297
+ socket.localAddress = makeIpAddress(boundAddress.family.toLowerCase(), boundAddress.address, boundAddress.port);
300
298
  }
301
299
  async function handleTcpConnect({ socketId, remoteAddress }) {
302
300
  const socket = sockets.get(socketId);
303
301
  const host = serializeIpAddress(remoteAddress);
304
302
  const port = remoteAddress.val.port;
305
- const tcp = socket.tcp = new Socket({
306
- handle: socket.handle,
307
- pauseOnCreate: true,
308
- allowHalfOpen: true
309
- });
303
+ const localAddress = socket.localAddress;
304
+ await closeTcpServer(socket);
305
+ const tcp = socket.tcp = new Socket({ allowHalfOpen: true });
306
+ tcp.setKeepAlive(socket.keepAliveEnabled, socket.keepAliveIdleTime);
310
307
  const onConnect = once(tcp, "connect");
311
308
  tcp.connect({
312
309
  port,
313
- host
310
+ host,
311
+ localAddress: localAddress ? serializeIpAddress(localAddress) : void 0,
312
+ localPort: localAddress?.val.port
314
313
  });
315
314
  await onConnect;
316
315
  }
317
316
  async function handleTcpListen({ socketId, stream }) {
318
317
  const writer = stream.getWriter();
319
318
  const socket = sockets.get(socketId);
320
- const { handle, backlog, family } = socket;
319
+ const { backlog, family } = socket;
320
+ socket.acceptWriter = writer;
321
+ let server = socket.server;
322
+ if (!server) {
323
+ server = socket.server = createTcpServer(socket);
324
+ const onListening = once(server, "listening");
325
+ server.listen({
326
+ host: family === "ipv6" ? "::" : "0.0.0.0",
327
+ port: 0,
328
+ backlog,
329
+ ipv6Only: family === "ipv6"
330
+ });
331
+ await onListening;
332
+ }
333
+ const addr = server.address();
334
+ if (addr && typeof addr === "object") socket.localAddress = makeIpAddress(family, addr.address, addr.port);
335
+ }
336
+ function createTcpServer(socket) {
321
337
  const server = new Server({
322
338
  pauseOnConnect: true,
323
339
  allowHalfOpen: true
324
340
  });
325
- const onListening = once(server, "listening");
326
- server.listen(handle, backlog);
327
- await onListening;
328
- const addr = server.address();
329
- if (addr && typeof addr === "object") socket.localAddress = makeIpAddress(family, addr.address, addr.port);
330
341
  server.on("connection", (conn) => {
342
+ const writer = socket.acceptWriter;
343
+ if (!writer) {
344
+ conn.destroy();
345
+ return;
346
+ }
331
347
  conn.allowHalfOpen = true;
348
+ conn.setKeepAlive(socket.keepAliveEnabled, socket.keepAliveIdleTime);
332
349
  const id = NEXT_SOCKET_ID++;
333
350
  sockets.set(id, {
334
- handle: conn._handle,
335
- family,
336
- backlog,
351
+ family: socket.family,
352
+ backlog: socket.backlog,
337
353
  tcp: conn,
338
354
  server: null,
339
- localAddress: makeIpAddress(family, conn.localAddress, conn.localPort),
355
+ acceptWriter: null,
356
+ localAddress: makeIpAddress(socket.family, conn.localAddress, conn.localPort),
357
+ keepAliveEnabled: socket.keepAliveEnabled,
358
+ keepAliveIdleTime: socket.keepAliveIdleTime,
340
359
  disposed: false,
341
360
  activeStreams: 0
342
361
  });
343
362
  writer.write({
344
- family,
363
+ family: socket.family,
345
364
  socketId: id
346
365
  });
347
366
  });
348
- socket.server = server;
349
- server.on("error", (err) => stream.abort(err));
350
- server.on("close", () => writer.close());
367
+ server.on("error", (err) => socket.acceptWriter?.abort(err));
368
+ server.on("close", () => socket.acceptWriter?.close());
369
+ return server;
370
+ }
371
+ function closeTcpServer(socket) {
372
+ const server = socket.server;
373
+ socket.server = null;
374
+ if (!server) return Promise.resolve();
375
+ return new Promise((resolve, reject) => {
376
+ server.close((err) => err ? reject(err) : resolve());
377
+ });
351
378
  }
352
379
  async function handleTcpSend({ socketId, stream }) {
353
380
  const socket = sockets.get(socketId);
354
381
  socket.activeStreams++;
355
382
  const { tcp } = socket;
356
- const readable = Readable.fromWeb(stream);
383
+ const reader = stream.getReader();
384
+ let rejectOnSocketError;
385
+ const socketError = new Promise((_resolve, reject) => {
386
+ rejectOnSocketError = reject;
387
+ });
388
+ tcp.once("error", rejectOnSocketError);
357
389
  try {
358
- await pipeline(readable, tcp);
390
+ while (true) {
391
+ const { done, value } = await Promise.race([reader.read(), socketError]);
392
+ if (done) break;
393
+ await Promise.race([new Promise((resolve, reject) => {
394
+ tcp.write(value, (err) => err ? reject(err) : resolve());
395
+ }), socketError]);
396
+ }
397
+ await new Promise((resolve, reject) => {
398
+ tcp.end((err) => err ? reject(err) : resolve());
399
+ });
400
+ } catch (err) {
401
+ reader.cancel(err).catch(() => {});
402
+ throw err;
359
403
  } finally {
404
+ tcp.off("error", rejectOnSocketError);
405
+ reader.releaseLock();
360
406
  socket.activeStreams--;
361
407
  cleanupDisposedSocket(socketId, socket);
362
408
  }
@@ -422,22 +468,19 @@ function cleanupDisposedSocket(socketId, socket) {
422
468
  if (!socket.disposed || socket.activeStreams > 0) return;
423
469
  if (socket.server) socket.server.close();
424
470
  if (socket.tcp) socket.tcp.destroy();
425
- if (socket.handle) socket.handle.close();
426
471
  sockets.delete(socketId);
427
472
  }
428
473
  async function handleGetLocalAddress({ socketId }) {
429
474
  const socket = sockets.get(socketId);
430
- const out = {};
431
- const code = socket.handle.getsockname(out);
432
- if (code !== 0) throw SocketError.from(-code);
433
- return makeIpAddress(out.family.toLowerCase(), out.address, out.port);
475
+ const address = socket.tcp?.address();
476
+ if (address && typeof address !== "string" && "family" in address) return makeIpAddress(address.family.toLowerCase(), address.address, address.port);
477
+ if (socket.localAddress) return socket.localAddress;
478
+ throw new SocketError("invalid-state");
434
479
  }
435
480
  async function handleGetRemoteAddress({ socketId }) {
436
481
  const socket = sockets.get(socketId);
437
- const out = {};
438
- const code = socket.handle.getpeername(out);
439
- if (code !== 0) throw SocketError.from(-code);
440
- return makeIpAddress(out.family.toLowerCase(), out.address, out.port);
482
+ if (!socket.tcp?.remoteFamily || !socket.tcp.remoteAddress || !socket.tcp.remotePort) throw new SocketError("invalid-state");
483
+ return makeIpAddress(socket.tcp.remoteFamily.toLowerCase(), socket.tcp.remoteAddress, socket.tcp.remotePort);
441
484
  }
442
485
  function handleTcpSetBacklogSize({ socketId, value }) {
443
486
  const socket = sockets.get(socketId);
@@ -445,9 +488,9 @@ function handleTcpSetBacklogSize({ socketId, value }) {
445
488
  }
446
489
  async function handleTcpSetKeepAlive({ socketId, keepAliveEnabled, keepAliveIdleTime }) {
447
490
  const socket = sockets.get(socketId);
448
- const time = Number(keepAliveIdleTime / 1000000000n);
449
- const code = socket.handle.setKeepAlive(keepAliveEnabled, time);
450
- if (code !== 0) throw SocketError.from(-code);
491
+ socket.keepAliveEnabled = keepAliveEnabled;
492
+ socket.keepAliveIdleTime = Number(keepAliveIdleTime / 1000000000n);
493
+ socket.tcp?.setKeepAlive(socket.keepAliveEnabled, socket.keepAliveIdleTime);
451
494
  }
452
495
  async function handleRecvBufferSize({ socketId }) {
453
496
  const socket = sockets.get(socketId);
@@ -1,12 +1,8 @@
1
1
  import { Socket, Server } from "node:net";
2
- import { Readable } from "stream";
3
- import { pipeline } from "stream/promises";
4
2
  import { once } from "node:events";
5
3
  import { Router } from "../workers/resource-worker.js";
6
4
  import { serializeIpAddress, makeIpAddress, ipAddressConflict } from "../sockets/address.js";
7
5
  import { SocketError } from "../sockets/error.js";
8
- import process from "node:process";
9
- const { TCP, constants: TCPConstants } = process.binding("tcp_wrap");
10
6
  // Socket instances stored by ID
11
7
  const sockets = new Map();
12
8
  // Unique IDs for sockets
@@ -34,14 +30,15 @@ Router()
34
30
  // Create a new TCP socket
35
31
  function handleTcpCreate({ family }) {
36
32
  const socketId = NEXT_SOCKET_ID++;
37
- const handle = new TCP(TCPConstants.SOCKET);
38
33
  sockets.set(socketId, {
39
- handle,
40
34
  family,
41
35
  tcp: null,
42
36
  server: null,
37
+ acceptWriter: null,
43
38
  backlog: 128,
44
39
  localAddress: null,
40
+ keepAliveEnabled: false,
41
+ keepAliveIdleTime: 0,
45
42
  disposed: false,
46
43
  activeStreams: 0,
47
44
  });
@@ -52,48 +49,49 @@ async function handleTcpBind({ socketId, localAddress }) {
52
49
  const socket = sockets.get(socketId);
53
50
  const address = serializeIpAddress(localAddress);
54
51
  const port = localAddress.val.port;
55
- const { handle, family } = socket;
52
+ const { family } = socket;
56
53
  const hasConflict = [...sockets].some(([id, { localAddress: boundAddress }]) => id !== socketId && boundAddress && ipAddressConflict(boundAddress, localAddress));
57
54
  if (hasConflict) {
58
55
  const err = new Error("EADDRINUSE");
59
56
  err.code = "EADDRINUSE";
60
57
  throw err;
61
58
  }
62
- await new Promise((resolve, reject) => {
63
- let code;
64
- if (family === "ipv6") {
65
- code = handle.bind6(address, port, TCPConstants.UV_TCP_IPV6ONLY);
66
- }
67
- else {
68
- code = handle.bind(address, port);
69
- }
70
- if (code !== 0) {
71
- reject(SocketError.from(-code));
72
- }
73
- else {
74
- resolve();
75
- }
59
+ // node:net has no public standalone TCP bind API. Keep a server open
60
+ // to reserve the endpoint, then reuse it for listen or close it for connect.
61
+ const server = (socket.server = createTcpServer(socket));
62
+ const onListening = once(server, "listening");
63
+ server.listen({
64
+ host: address,
65
+ port,
66
+ backlog: socket.backlog,
67
+ ipv6Only: family === "ipv6",
76
68
  });
77
- const out = {};
78
- const code = handle.getsockname(out);
79
- if (code !== 0) {
80
- throw SocketError.from(-code);
69
+ await onListening;
70
+ const boundAddress = server.address();
71
+ if (!boundAddress || typeof boundAddress === "string") {
72
+ throw new SocketError("invalid-state");
81
73
  }
82
- socket.localAddress = makeIpAddress(out.family.toLowerCase(), out.address, out.port);
74
+ socket.localAddress = makeIpAddress(boundAddress.family.toLowerCase(), boundAddress.address, boundAddress.port);
83
75
  }
84
76
  // Connect a socket to remote address
85
77
  async function handleTcpConnect({ socketId, remoteAddress }) {
86
78
  const socket = sockets.get(socketId);
87
79
  const host = serializeIpAddress(remoteAddress);
88
80
  const port = remoteAddress.val.port;
81
+ const localAddress = socket.localAddress;
82
+ await closeTcpServer(socket);
89
83
  const tcp = (socket.tcp = new Socket({
90
- handle: socket.handle,
91
- pauseOnCreate: true,
92
84
  allowHalfOpen: true,
93
85
  }));
86
+ tcp.setKeepAlive(socket.keepAliveEnabled, socket.keepAliveIdleTime);
94
87
  // TODO(tandr): Add lookup
95
88
  const onConnect = once(tcp, "connect");
96
- tcp.connect({ port, host });
89
+ tcp.connect({
90
+ port,
91
+ host,
92
+ localAddress: localAddress ? serializeIpAddress(localAddress) : undefined,
93
+ localPort: localAddress?.val.port,
94
+ });
97
95
  // events.once rejects when the emitter produces an error while waiting and
98
96
  // removes its temporary listeners after settling. A separate error promise
99
97
  // would remain attached after a successful connect and could later reject
@@ -103,46 +101,111 @@ async function handleTcpConnect({ socketId, remoteAddress }) {
103
101
  async function handleTcpListen({ socketId, stream }) {
104
102
  const writer = stream.getWriter();
105
103
  const socket = sockets.get(socketId);
106
- const { handle, backlog, family } = socket;
107
- const server = new Server({
108
- pauseOnConnect: true,
109
- allowHalfOpen: true,
110
- });
111
- const onListening = once(server, "listening");
112
- server.listen(handle, backlog);
113
- await onListening;
104
+ const { backlog, family } = socket;
105
+ socket.acceptWriter = writer;
106
+ let server = socket.server;
107
+ if (!server) {
108
+ server = socket.server = createTcpServer(socket);
109
+ const onListening = once(server, "listening");
110
+ server.listen({
111
+ host: family === "ipv6" ? "::" : "0.0.0.0",
112
+ port: 0,
113
+ backlog,
114
+ ipv6Only: family === "ipv6",
115
+ });
116
+ await onListening;
117
+ }
114
118
  const addr = server.address();
115
119
  if (addr && typeof addr === "object") {
116
120
  socket.localAddress = makeIpAddress(family, addr.address, addr.port);
117
121
  }
122
+ }
123
+ function createTcpServer(socket) {
124
+ const server = new Server({
125
+ pauseOnConnect: true,
126
+ allowHalfOpen: true,
127
+ });
118
128
  server.on("connection", (conn) => {
129
+ const writer = socket.acceptWriter;
130
+ // The reservation is already listening at the OS level, so reject any
131
+ // connections that arrive before the WASI socket enters its listen state.
132
+ if (!writer) {
133
+ conn.destroy();
134
+ return;
135
+ }
119
136
  conn.allowHalfOpen = true;
137
+ conn.setKeepAlive(socket.keepAliveEnabled, socket.keepAliveIdleTime);
120
138
  const id = NEXT_SOCKET_ID++;
121
139
  sockets.set(id, {
122
- handle: conn._handle,
123
- family,
124
- backlog,
140
+ family: socket.family,
141
+ backlog: socket.backlog,
125
142
  tcp: conn,
126
143
  server: null,
127
- localAddress: makeIpAddress(family, conn.localAddress, conn.localPort),
144
+ acceptWriter: null,
145
+ localAddress: makeIpAddress(socket.family, conn.localAddress, conn.localPort),
146
+ keepAliveEnabled: socket.keepAliveEnabled,
147
+ keepAliveIdleTime: socket.keepAliveIdleTime,
128
148
  disposed: false,
129
149
  activeStreams: 0,
130
150
  });
131
- writer.write({ family, socketId: id });
151
+ writer.write({ family: socket.family, socketId: id });
152
+ });
153
+ server.on("error", (err) => socket.acceptWriter?.abort(err));
154
+ server.on("close", () => socket.acceptWriter?.close());
155
+ return server;
156
+ }
157
+ function closeTcpServer(socket) {
158
+ const server = socket.server;
159
+ socket.server = null;
160
+ if (!server) {
161
+ return Promise.resolve();
162
+ }
163
+ return new Promise((resolve, reject) => {
164
+ server.close((err) => (err ? reject(err) : resolve()));
132
165
  });
133
- socket.server = server;
134
- server.on("error", (err) => stream.abort(err));
135
- server.on("close", () => writer.close());
136
166
  }
137
167
  async function handleTcpSend({ socketId, stream }) {
138
168
  const socket = sockets.get(socketId);
139
169
  socket.activeStreams++;
140
170
  const { tcp } = socket;
141
- const readable = Readable.fromWeb(stream);
171
+ const reader = stream.getReader();
172
+ let rejectOnSocketError;
173
+ const socketError = new Promise((_resolve, reject) => {
174
+ rejectOnSocketError = reject;
175
+ });
176
+ tcp.once("error", rejectOnSocketError);
142
177
  try {
143
- await pipeline(readable, tcp);
178
+ // Do not replace this pump with stream.pipeline(). Node 26 changed async
179
+ // iteration to preserve allowHalfOpen duplex streams. For WASI TCP send,
180
+ // however, a write failure must promptly cancel the transferred input
181
+ // stream so the guest's stream writer is dropped. Explicitly awaiting each
182
+ // write keeps socket backpressure and cancellation coupled across workers.
183
+ while (true) {
184
+ const { done, value } = await Promise.race([reader.read(), socketError]);
185
+ if (done) {
186
+ break;
187
+ }
188
+ await Promise.race([
189
+ new Promise((resolve, reject) => {
190
+ tcp.write(value, (err) => (err ? reject(err) : resolve()));
191
+ }),
192
+ socketError,
193
+ ]);
194
+ }
195
+ await new Promise((resolve, reject) => {
196
+ tcp.end((err) => (err ? reject(err) : resolve()));
197
+ });
198
+ }
199
+ catch (err) {
200
+ // Cancellation crosses the worker boundary. Do not wait for its completion:
201
+ // the producer can itself be waiting for this operation's result, which
202
+ // would turn error propagation into a cycle.
203
+ void reader.cancel(err).catch(() => { });
204
+ throw err;
144
205
  }
145
206
  finally {
207
+ tcp.off("error", rejectOnSocketError);
208
+ reader.releaseLock();
146
209
  socket.activeStreams--;
147
210
  cleanupDisposedSocket(socketId, socket);
148
211
  }
@@ -229,28 +292,25 @@ function cleanupDisposedSocket(socketId, socket) {
229
292
  if (socket.tcp) {
230
293
  socket.tcp.destroy();
231
294
  }
232
- if (socket.handle) {
233
- socket.handle.close();
234
- }
235
295
  sockets.delete(socketId);
236
296
  }
237
297
  async function handleGetLocalAddress({ socketId }) {
238
298
  const socket = sockets.get(socketId);
239
- const out = {};
240
- const code = socket.handle.getsockname(out);
241
- if (code !== 0) {
242
- throw SocketError.from(-code);
299
+ const address = socket.tcp?.address();
300
+ if (address && typeof address !== "string" && "family" in address) {
301
+ return makeIpAddress(address.family.toLowerCase(), address.address, address.port);
243
302
  }
244
- return makeIpAddress(out.family.toLowerCase(), out.address, out.port);
303
+ if (socket.localAddress) {
304
+ return socket.localAddress;
305
+ }
306
+ throw new SocketError("invalid-state");
245
307
  }
246
308
  async function handleGetRemoteAddress({ socketId }) {
247
309
  const socket = sockets.get(socketId);
248
- const out = {};
249
- const code = socket.handle.getpeername(out);
250
- if (code !== 0) {
251
- throw SocketError.from(-code);
310
+ if (!socket.tcp?.remoteFamily || !socket.tcp.remoteAddress || !socket.tcp.remotePort) {
311
+ throw new SocketError("invalid-state");
252
312
  }
253
- return makeIpAddress(out.family.toLowerCase(), out.address, out.port);
313
+ return makeIpAddress(socket.tcp.remoteFamily.toLowerCase(), socket.tcp.remoteAddress, socket.tcp.remotePort);
254
314
  }
255
315
  function handleTcpSetBacklogSize({ socketId, value }) {
256
316
  const socket = sockets.get(socketId);
@@ -258,11 +318,9 @@ function handleTcpSetBacklogSize({ socketId, value }) {
258
318
  }
259
319
  async function handleTcpSetKeepAlive({ socketId, keepAliveEnabled, keepAliveIdleTime }) {
260
320
  const socket = sockets.get(socketId);
261
- const time = Number(keepAliveIdleTime / 1000000000n);
262
- const code = socket.handle.setKeepAlive(keepAliveEnabled, time);
263
- if (code !== 0) {
264
- throw SocketError.from(-code);
265
- }
321
+ socket.keepAliveEnabled = keepAliveEnabled;
322
+ socket.keepAliveIdleTime = Number(keepAliveIdleTime / 1000000000n);
323
+ socket.tcp?.setKeepAlive(socket.keepAliveEnabled, socket.keepAliveIdleTime);
266
324
  }
267
325
  async function handleRecvBufferSize({ socketId }) {
268
326
  const socket = sockets.get(socketId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytecodealliance/preview3-shim",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "WASI Preview3 shim for JS environments",
5
5
  "homepage": "https://github.com/bytecodealliance/jco/blob/main/packages/preview3-shim/#readme",
6
6
  "bugs": {
@@ -48,7 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@bytecodealliance/preview2-shim": "^0.19.0"
51
+ "@bytecodealliance/jco-node-fs": "0.2.0",
52
+ "@bytecodealliance/preview2-shim": "^0.22.0"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/node": "^24.12.4",