@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.
- package/dist/nodejs/filesystem/descriptor.d.ts +1 -1
- package/dist/nodejs/filesystem/descriptor.js +8 -5
- package/dist/nodejs/sockets/tcp.js +7 -1
- package/dist/nodejs/stream.d.ts +7 -1
- package/dist/nodejs/stream.js +4 -0
- package/dist/nodejs/workers/tcp-worker.bundle.js +94 -51
- package/dist/nodejs/workers/tcp-worker.js +123 -65
- package/package.json +3 -2
|
@@ -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(
|
|
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(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/nodejs/stream.d.ts
CHANGED
|
@@ -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?:
|
|
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.
|
package/dist/nodejs/stream.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
const
|
|
298
|
-
if (
|
|
299
|
-
socket.localAddress = makeIpAddress(
|
|
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
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 {
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
backlog,
|
|
351
|
+
family: socket.family,
|
|
352
|
+
backlog: socket.backlog,
|
|
337
353
|
tcp: conn,
|
|
338
354
|
server: null,
|
|
339
|
-
|
|
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
|
-
|
|
349
|
-
server.on("
|
|
350
|
-
server
|
|
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
|
|
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
|
-
|
|
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
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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 {
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
throw SocketError
|
|
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(
|
|
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({
|
|
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 {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
backlog,
|
|
140
|
+
family: socket.family,
|
|
141
|
+
backlog: socket.backlog,
|
|
125
142
|
tcp: conn,
|
|
126
143
|
server: null,
|
|
127
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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(
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
"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/
|
|
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",
|