@polyengine/wasi 0.1.0-pre.g633468a
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/LICENSE +202 -0
- package/README.md +23 -0
- package/esm/cli.js +160 -0
- package/esm/cli_stdio.js +203 -0
- package/esm/clocks.js +84 -0
- package/esm/filesystem.js +83 -0
- package/esm/filesystem_node.js +463 -0
- package/esm/filesystem_web.js +315 -0
- package/esm/http.js +663 -0
- package/esm/internal/cli_shared.js +26 -0
- package/esm/internal/fs_provider.js +795 -0
- package/esm/internal/sockets_02.js +992 -0
- package/esm/internal/sockets_03.js +969 -0
- package/esm/internal/sockets_platform.js +530 -0
- package/esm/internal/sockets_shared.js +268 -0
- package/esm/io.js +643 -0
- package/esm/mod.js +80 -0
- package/esm/package.json +3 -0
- package/esm/random.js +96 -0
- package/esm/sockets.js +172 -0
- package/package.json +98 -0
- package/types/cli.d.ts +44 -0
- package/types/cli_stdio.d.ts +31 -0
- package/types/clocks.d.ts +8 -0
- package/types/filesystem.d.ts +30 -0
- package/types/filesystem_node.d.ts +16 -0
- package/types/filesystem_web.d.ts +61 -0
- package/types/http.d.ts +169 -0
- package/types/internal/cli_shared.d.ts +26 -0
- package/types/internal/fs_provider.d.ts +174 -0
- package/types/internal/sockets_02.d.ts +37 -0
- package/types/internal/sockets_03.d.ts +12 -0
- package/types/internal/sockets_platform.d.ts +112 -0
- package/types/internal/sockets_shared.d.ts +256 -0
- package/types/io.d.ts +177 -0
- package/types/mod.d.ts +31 -0
- package/types/random.d.ts +17 -0
- package/types/sockets.d.ts +22 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// The platform seam under `sockets.ts`: the connection shapes the
|
|
2
|
+
// providers drive (`DatagramConn`, `TcpConn`, `TcpListener`), served by
|
|
3
|
+
// ONE backend — the node builtins (`node:dgram` / `node:net`), resolved
|
|
4
|
+
// through `process.getBuiltinModule` (synchronous, Node >= 20.16 / 22.3;
|
|
5
|
+
// no static `node:` imports, so the module graph stays bundler- and
|
|
6
|
+
// browser-safe).
|
|
7
|
+
//
|
|
8
|
+
// Why node builtins EVERYWHERE, including on Deno (a deliberate reversal
|
|
9
|
+
// of the earlier native-first split, with better reasoning):
|
|
10
|
+
//
|
|
11
|
+
// * Deno ships these builtins as STABLE node-compat surface. Its own
|
|
12
|
+
// `Deno.listenDatagram` sits behind `--unstable-net`, but that flag
|
|
13
|
+
// gates the native API's SHAPE stability, not the capability — so
|
|
14
|
+
// the old backend told stock-Deno consumers "UDP not-supported"
|
|
15
|
+
// while the platform's stable surface could serve it. One backend
|
|
16
|
+
// drops the `--unstable-net` requirement entirely.
|
|
17
|
+
// * One behavior on every runtime: one divergence table, one test
|
|
18
|
+
// matrix (the whole provider suite exercises this path under Deno's
|
|
19
|
+
// compat; `just test-sockets-node` re-runs it on genuine Node).
|
|
20
|
+
// * `net.connect` exposes `localAddress`/`localPort`, so
|
|
21
|
+
// connect-from-bound works — the native `Deno.connect` never could.
|
|
22
|
+
// * The engines where node builtins do not exist and JSPI is absent
|
|
23
|
+
// (JSC, and Bun atop it) cannot run polyengine guests anyway: JSC lacks
|
|
24
|
+
// multi-memory. Browsers have no sockets of any flavor.
|
|
25
|
+
//
|
|
26
|
+
// Costs, measured and accepted:
|
|
27
|
+
// * ~2x per-operation overhead on a UDP loopback ping-pong microbench
|
|
28
|
+
// vs the native API (~105k vs ~158k pkt/s round-trips under Deno) —
|
|
29
|
+
// far above what the QUIC consumers draw, and the CM boundary
|
|
30
|
+
// dominates real flows.
|
|
31
|
+
// * Permission denials arrive as genuine `Deno.errors.NotCapable`
|
|
32
|
+
// instances through the compat layer (verified) — the provider's
|
|
33
|
+
// error mapper keeps its Deno-class checks for exactly this.
|
|
34
|
+
//
|
|
35
|
+
// Node adapter notes (each verified empirically on pinned node 26.7.0,
|
|
36
|
+
// system node 24, and Deno's node-compat):
|
|
37
|
+
//
|
|
38
|
+
// * dgram bind is made SYNCHRONOUS by giving `createSocket` a custom
|
|
39
|
+
// `lookup` whose callback fires synchronously (addresses here are
|
|
40
|
+
// always numeric, so there is nothing to resolve). The WIT `bind` and
|
|
41
|
+
// `get-local-address` are sync funcs; with the default async lookup,
|
|
42
|
+
// `address()` right after `bind()` throws EBADF and bind errors only
|
|
43
|
+
// surface on a later tick. With the sync lookup, `address()` is valid
|
|
44
|
+
// on return and EADDRINUSE throws at the bind call site.
|
|
45
|
+
// * `net.Server.listen` has NO such escape hatch: with a specific host
|
|
46
|
+
// the OS bind is DEFERRED one event-loop turn (`'listening'` /
|
|
47
|
+
// `'error'` arrive later). The seam exposes that settle as
|
|
48
|
+
// `TcpListener.settled()`; the provider awaits it inside a
|
|
49
|
+
// `suspending`-marked `listen`, parking the calling guest frame for
|
|
50
|
+
// the one tick (embedder-api A1/A2 — the same kernel that serves
|
|
51
|
+
// wasi:io's sync `block`). Full listener fidelity follows: real
|
|
52
|
+
// ephemeral addresses, real bind error codes.
|
|
53
|
+
// * dgram receive is push-shaped (`'message'` events); the adapter
|
|
54
|
+
// bridges to the seam's pull shape with a BOUNDED queue
|
|
55
|
+
// (tail-drop past `MAX_QUEUED_DATAGRAMS` — kernel-buffer semantics:
|
|
56
|
+
// UDP is lossy by contract, and a guest that stops reading must not
|
|
57
|
+
// grow host memory without bound).
|
|
58
|
+
// * `net.connect` gets `allowHalfOpen: true` — Node's default auto-ends
|
|
59
|
+
// the write side on peer FIN, which would break the WIT's
|
|
60
|
+
// shared-ownership/half-close contract (the send stream must remain
|
|
61
|
+
// usable after the receive side ends).
|
|
62
|
+
// * TCP reads pull via `'readable'` + `read()`, handing node's own
|
|
63
|
+
// buffers through (zero extra copy); the excess past the caller's
|
|
64
|
+
// `max` is `unshift()`ed back.
|
|
65
|
+
//
|
|
66
|
+
// The adapters throw RAW platform errors (node `code`-carrying errors,
|
|
67
|
+
// and `Deno.errors.NotCapable` where the compat layer raises it); mapping
|
|
68
|
+
// onto the WIT `error-code` vocabulary stays with the providers
|
|
69
|
+
// (`sockets.ts` `mapPlatformError`), so this module has no imports at
|
|
70
|
+
// all. Everything is looked up through `globalThis` at call time — the
|
|
71
|
+
// module never assumes a platform at evaluation, and `create` can answer
|
|
72
|
+
// `not-supported` truthfully on hosts with no node builtins (browsers).
|
|
73
|
+
// --- detection ----------------------------------------------------------------
|
|
74
|
+
/** `process.getBuiltinModule(name)`, if this host has it (Node, Deno, Bun). */
|
|
75
|
+
function nodeBuiltin(name) {
|
|
76
|
+
const proc = globalThis.process;
|
|
77
|
+
const get = proc?.getBuiltinModule;
|
|
78
|
+
if (typeof get !== "function")
|
|
79
|
+
return undefined;
|
|
80
|
+
try {
|
|
81
|
+
return get.call(proc, name);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The datagram backend, re-detected per call. */
|
|
88
|
+
export function listenDatagram() {
|
|
89
|
+
return nodeListenDatagram();
|
|
90
|
+
}
|
|
91
|
+
/** The TCP-connect backend, re-detected per call. */
|
|
92
|
+
export function tcpConnect() {
|
|
93
|
+
return nodeTcpConnect();
|
|
94
|
+
}
|
|
95
|
+
/** The TCP-listen backend, re-detected per call. */
|
|
96
|
+
export function tcpListen() {
|
|
97
|
+
return nodeTcpListen();
|
|
98
|
+
}
|
|
99
|
+
/** The name-resolution backend, re-detected per call. */
|
|
100
|
+
export function dnsLookup() {
|
|
101
|
+
const dns = nodeBuiltin("node:dns");
|
|
102
|
+
const promises = dns?.promises;
|
|
103
|
+
const lookup = promises?.lookup;
|
|
104
|
+
if (promises === undefined || lookup === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
// verbatim: getaddrinfo order as-is (the WIT: "returned in the order
|
|
107
|
+
// the resolver prefers").
|
|
108
|
+
return (name) => lookup.call(promises, name, { all: true, verbatim: true });
|
|
109
|
+
}
|
|
110
|
+
// --- the node:dgram backend -----------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Queued-but-unread datagrams past this bound are dropped (tail-drop, the
|
|
113
|
+
* kernel-buffer analogue). Node's `'message'` push keeps delivering
|
|
114
|
+
* whether or not the guest reads; unread datagrams must not accumulate
|
|
115
|
+
* without bound.
|
|
116
|
+
*/
|
|
117
|
+
export const MAX_QUEUED_DATAGRAMS = 256;
|
|
118
|
+
function nodeListenDatagram() {
|
|
119
|
+
const dgram = nodeBuiltin("node:dgram");
|
|
120
|
+
const net = nodeBuiltin("node:net");
|
|
121
|
+
if (dgram === undefined || net === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
return ({ hostname, port }) => {
|
|
124
|
+
const socket = dgram.createSocket({
|
|
125
|
+
type: hostname.includes(":") ? "udp6" : "udp4",
|
|
126
|
+
// The synchronous-lookup trick (module header): makes bind()
|
|
127
|
+
// complete synchronously for numeric addresses.
|
|
128
|
+
lookup: (addr, _options, cb) => cb(null, addr, net.isIP(addr)),
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
socket.bind(port, hostname);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
try {
|
|
135
|
+
socket.close();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Never came up.
|
|
139
|
+
}
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
return new NodeDatagramConn(socket);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** A plain Error carrying a Node-style `code` (for the provider's mapper). */
|
|
146
|
+
function codedError(code, message) {
|
|
147
|
+
return Object.assign(new Error(message), { code });
|
|
148
|
+
}
|
|
149
|
+
class NodeDatagramConn {
|
|
150
|
+
addr;
|
|
151
|
+
#socket;
|
|
152
|
+
#queue = [];
|
|
153
|
+
#waiters = [];
|
|
154
|
+
#failure;
|
|
155
|
+
#closed = false;
|
|
156
|
+
/** Promise-swap wake for the poll-shaped consumers (waitReceive). */
|
|
157
|
+
#wake = () => { };
|
|
158
|
+
#wakePromise;
|
|
159
|
+
constructor(socket) {
|
|
160
|
+
this.#socket = socket;
|
|
161
|
+
this.#wakePromise = new Promise((r) => (this.#wake = r));
|
|
162
|
+
const a = socket.address();
|
|
163
|
+
this.addr = { transport: "udp", hostname: a.address, port: a.port };
|
|
164
|
+
socket.on("message", (...args) => {
|
|
165
|
+
const msg = args[0];
|
|
166
|
+
const rinfo = args[1];
|
|
167
|
+
const from = {
|
|
168
|
+
transport: "udp",
|
|
169
|
+
hostname: rinfo.address,
|
|
170
|
+
port: rinfo.port,
|
|
171
|
+
};
|
|
172
|
+
const waiter = this.#waiters.shift();
|
|
173
|
+
if (waiter !== undefined) {
|
|
174
|
+
waiter.resolve([msg, from]);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (this.#closed || this.#queue.length >= MAX_QUEUED_DATAGRAMS)
|
|
178
|
+
return; // tail-drop
|
|
179
|
+
this.#queue.push([msg, from]);
|
|
180
|
+
this.#signal();
|
|
181
|
+
});
|
|
182
|
+
socket.on("error", (...args) => {
|
|
183
|
+
this.#failure = args[0];
|
|
184
|
+
this.#failWaiters(args[0]);
|
|
185
|
+
this.#signal();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
#signal() {
|
|
189
|
+
const wake = this.#wake;
|
|
190
|
+
this.#wakePromise = new Promise((r) => (this.#wake = r));
|
|
191
|
+
wake();
|
|
192
|
+
}
|
|
193
|
+
#failWaiters(e) {
|
|
194
|
+
const waiters = this.#waiters;
|
|
195
|
+
this.#waiters = [];
|
|
196
|
+
for (const w of waiters)
|
|
197
|
+
w.reject(e);
|
|
198
|
+
}
|
|
199
|
+
send(p, addr) {
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
if (this.#closed) {
|
|
202
|
+
reject(codedError("ERR_SOCKET_DGRAM_NOT_RUNNING", "socket is closed"));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const cb = (err) => err !== null && err !== undefined ? reject(err) : resolve(p.length);
|
|
206
|
+
if (addr === undefined)
|
|
207
|
+
this.#socket.send(p, cb); // connected mode
|
|
208
|
+
else
|
|
209
|
+
this.#socket.send(p, addr.port, addr.hostname, cb);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
connect(addr) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
if (this.#closed) {
|
|
215
|
+
reject(codedError("ERR_SOCKET_DGRAM_NOT_RUNNING", "socket is closed"));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.#socket.connect(addr.port, addr.hostname, (err) => {
|
|
219
|
+
if (err !== null && err !== undefined)
|
|
220
|
+
reject(err);
|
|
221
|
+
else
|
|
222
|
+
resolve();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
disconnect() {
|
|
227
|
+
this.#socket.disconnect();
|
|
228
|
+
}
|
|
229
|
+
setTtl(ttl) {
|
|
230
|
+
this.#socket.setTTL(ttl);
|
|
231
|
+
}
|
|
232
|
+
getRecvBufferSize() {
|
|
233
|
+
return this.#socket.getRecvBufferSize();
|
|
234
|
+
}
|
|
235
|
+
setRecvBufferSize(size) {
|
|
236
|
+
this.#socket.setRecvBufferSize(size);
|
|
237
|
+
}
|
|
238
|
+
getSendBufferSize() {
|
|
239
|
+
return this.#socket.getSendBufferSize();
|
|
240
|
+
}
|
|
241
|
+
setSendBufferSize(size) {
|
|
242
|
+
this.#socket.setSendBufferSize(size);
|
|
243
|
+
}
|
|
244
|
+
receive() {
|
|
245
|
+
// Node hands each datagram as its own exactly-sized buffer, passed
|
|
246
|
+
// through directly.
|
|
247
|
+
if (this.#failure !== undefined)
|
|
248
|
+
return Promise.reject(this.#failure);
|
|
249
|
+
if (this.#closed) {
|
|
250
|
+
return Promise.reject(codedError("ERR_SOCKET_DGRAM_NOT_RUNNING", "socket is closed"));
|
|
251
|
+
}
|
|
252
|
+
const queued = this.#queue.shift();
|
|
253
|
+
if (queued !== undefined)
|
|
254
|
+
return Promise.resolve(queued);
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
this.#waiters.push({ resolve, reject });
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
tryReceive() {
|
|
260
|
+
if (this.#failure !== undefined)
|
|
261
|
+
throw this.#failure;
|
|
262
|
+
if (this.#closed) {
|
|
263
|
+
throw codedError("ERR_SOCKET_DGRAM_NOT_RUNNING", "socket is closed");
|
|
264
|
+
}
|
|
265
|
+
return this.#queue.shift();
|
|
266
|
+
}
|
|
267
|
+
receiveReady() {
|
|
268
|
+
return this.#queue.length > 0 || this.#failure !== undefined || this.#closed;
|
|
269
|
+
}
|
|
270
|
+
waitReceive() {
|
|
271
|
+
return this.#wakePromise;
|
|
272
|
+
}
|
|
273
|
+
close() {
|
|
274
|
+
if (this.#closed)
|
|
275
|
+
return;
|
|
276
|
+
this.#closed = true;
|
|
277
|
+
// A parked receive settles as an error mapping onto `invalid-state`.
|
|
278
|
+
this.#failWaiters(codedError("ERR_SOCKET_DGRAM_NOT_RUNNING", "socket closed under a pending receive"));
|
|
279
|
+
this.#signal();
|
|
280
|
+
try {
|
|
281
|
+
this.#socket.close();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Already closed.
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function nodeTcpConnect() {
|
|
289
|
+
const net = nodeBuiltin("node:net");
|
|
290
|
+
if (net === undefined)
|
|
291
|
+
return undefined;
|
|
292
|
+
return async ({ hostname, port, localHostname, localPort }) => {
|
|
293
|
+
// allowHalfOpen is load-bearing (module header): the WIT half-close
|
|
294
|
+
// contract needs the write side to survive the peer's FIN.
|
|
295
|
+
const socket = net.connect({
|
|
296
|
+
host: hostname,
|
|
297
|
+
port,
|
|
298
|
+
allowHalfOpen: true,
|
|
299
|
+
...(localHostname === undefined ? {} : {
|
|
300
|
+
localAddress: localHostname,
|
|
301
|
+
localPort: localPort ?? 0,
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
await new Promise((resolve, reject) => {
|
|
305
|
+
const onError = (...args) => reject(args[0]);
|
|
306
|
+
socket.once("error", onError);
|
|
307
|
+
socket.once("connect", () => {
|
|
308
|
+
socket.off("error", onError);
|
|
309
|
+
resolve();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
return new NodeTcpConn(socket);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
class NodeTcpConn {
|
|
316
|
+
localAddr;
|
|
317
|
+
remoteAddr;
|
|
318
|
+
#socket;
|
|
319
|
+
#failure;
|
|
320
|
+
#ended = false;
|
|
321
|
+
constructor(socket) {
|
|
322
|
+
this.#socket = socket;
|
|
323
|
+
this.localAddr = {
|
|
324
|
+
transport: "tcp",
|
|
325
|
+
hostname: socket.localAddress ?? "",
|
|
326
|
+
port: socket.localPort ?? 0,
|
|
327
|
+
};
|
|
328
|
+
this.remoteAddr = {
|
|
329
|
+
transport: "tcp",
|
|
330
|
+
hostname: socket.remoteAddress ?? "",
|
|
331
|
+
port: socket.remotePort ?? 0,
|
|
332
|
+
};
|
|
333
|
+
// A persistent listener: an unhandled 'error' event would throw
|
|
334
|
+
// process-wide. In-flight reads observe #failure; writes get the
|
|
335
|
+
// error through their own callbacks.
|
|
336
|
+
socket.on("error", (...args) => {
|
|
337
|
+
this.#failure = args[0];
|
|
338
|
+
});
|
|
339
|
+
socket.on("end", () => {
|
|
340
|
+
this.#ended = true;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async read(max) {
|
|
344
|
+
for (;;) {
|
|
345
|
+
if (this.#failure !== undefined)
|
|
346
|
+
throw this.#failure;
|
|
347
|
+
const chunk = this.#socket.read();
|
|
348
|
+
if (chunk !== null) {
|
|
349
|
+
if (chunk.length > max) {
|
|
350
|
+
this.#socket.unshift(chunk.subarray(max));
|
|
351
|
+
return chunk.subarray(0, max);
|
|
352
|
+
}
|
|
353
|
+
return chunk;
|
|
354
|
+
}
|
|
355
|
+
if (this.#ended || this.#socket.readableEnded)
|
|
356
|
+
return null; // peer FIN
|
|
357
|
+
if (this.#socket.destroyed) {
|
|
358
|
+
// Locally destroyed under a pending read: never a fake EOS — maps
|
|
359
|
+
// onto invalid-state.
|
|
360
|
+
throw codedError("ERR_STREAM_DESTROYED", "socket closed under a pending read");
|
|
361
|
+
}
|
|
362
|
+
await new Promise((resolve) => {
|
|
363
|
+
const done = () => {
|
|
364
|
+
this.#socket.off("readable", done);
|
|
365
|
+
this.#socket.off("end", done);
|
|
366
|
+
this.#socket.off("error", done);
|
|
367
|
+
this.#socket.off("close", done);
|
|
368
|
+
resolve();
|
|
369
|
+
};
|
|
370
|
+
this.#socket.once("readable", done);
|
|
371
|
+
this.#socket.once("end", done);
|
|
372
|
+
this.#socket.once("error", done);
|
|
373
|
+
this.#socket.once("close", done);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
write(p) {
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
try {
|
|
380
|
+
// The callback fires once the chunk is handed to the kernel —
|
|
381
|
+
// awaiting it per-write is the backpressure.
|
|
382
|
+
this.#socket.write(p, (err) => {
|
|
383
|
+
if (err !== null && err !== undefined)
|
|
384
|
+
reject(err);
|
|
385
|
+
else
|
|
386
|
+
resolve(p.length);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
// write() itself throws after end()/destroy() (ERR_STREAM_*).
|
|
391
|
+
reject(e);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
closeWrite() {
|
|
396
|
+
return new Promise((resolve) => {
|
|
397
|
+
this.#socket.end(resolve);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
setKeepAlive(enabled, idleMs) {
|
|
401
|
+
this.#socket.setKeepAlive(enabled, idleMs);
|
|
402
|
+
}
|
|
403
|
+
close() {
|
|
404
|
+
this.#socket.destroy();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// --- the node:net listener --------------------------------------------------------
|
|
408
|
+
/**
|
|
409
|
+
* Accepted-but-unread connections past this bound are REFUSED (destroyed)
|
|
410
|
+
* — node's `'connection'` push keeps accepting whether or not the guest
|
|
411
|
+
* reads the accept stream, and unlike datagrams an accepted connection is
|
|
412
|
+
* a live socket, so tail-drop here means an active refusal rather than a
|
|
413
|
+
* silent discard (the polymorph-iroh#56 stance).
|
|
414
|
+
*/
|
|
415
|
+
export const MAX_QUEUED_CONNECTIONS = 64;
|
|
416
|
+
function nodeTcpListen() {
|
|
417
|
+
const net = nodeBuiltin("node:net");
|
|
418
|
+
if (net === undefined)
|
|
419
|
+
return undefined;
|
|
420
|
+
return ({ hostname, port, backlog }) => {
|
|
421
|
+
const queue = [];
|
|
422
|
+
const waiters = [];
|
|
423
|
+
let failure;
|
|
424
|
+
let closed = false;
|
|
425
|
+
// Promise-swap wake for the poll-shaped consumers (waitAccept).
|
|
426
|
+
let wake = () => { };
|
|
427
|
+
let wakePromise = new Promise((r) => (wake = r));
|
|
428
|
+
const signal = () => {
|
|
429
|
+
const w = wake;
|
|
430
|
+
wakePromise = new Promise((r) => (wake = r));
|
|
431
|
+
w();
|
|
432
|
+
};
|
|
433
|
+
const failWaiters = (e) => {
|
|
434
|
+
const w = waiters.splice(0, waiters.length);
|
|
435
|
+
for (const waiter of w)
|
|
436
|
+
waiter.reject(e);
|
|
437
|
+
};
|
|
438
|
+
const server = net.createServer({ allowHalfOpen: true }, (socket) => {
|
|
439
|
+
const waiter = waiters.shift();
|
|
440
|
+
if (waiter !== undefined) {
|
|
441
|
+
waiter.resolve(new NodeTcpConn(socket));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (closed || queue.length >= MAX_QUEUED_CONNECTIONS) {
|
|
445
|
+
socket.destroy(); // refuse: nobody is going to take it
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
queue.push(socket);
|
|
449
|
+
signal();
|
|
450
|
+
});
|
|
451
|
+
// The deferred OS bind (module header): 'listening' or 'error' arrive
|
|
452
|
+
// one tick after listen(). `settled` is created eagerly (with a no-op
|
|
453
|
+
// catch so an unobserved rejection cannot escape) and the provider
|
|
454
|
+
// awaits it inside its suspending `listen`.
|
|
455
|
+
let settle;
|
|
456
|
+
let fail;
|
|
457
|
+
const settledOnce = new Promise((resolve, reject) => {
|
|
458
|
+
settle = resolve;
|
|
459
|
+
fail = reject;
|
|
460
|
+
});
|
|
461
|
+
settledOnce.catch(() => {
|
|
462
|
+
// Observed via settled(); this guard only prevents an unhandled
|
|
463
|
+
// rejection if the provider never gets the chance.
|
|
464
|
+
});
|
|
465
|
+
server.on("listening", () => settle());
|
|
466
|
+
server.on("error", (...args) => {
|
|
467
|
+
failure = args[0];
|
|
468
|
+
fail(args[0]);
|
|
469
|
+
failWaiters(args[0]);
|
|
470
|
+
signal();
|
|
471
|
+
});
|
|
472
|
+
server.listen({ port, host: hostname, ...(backlog === undefined ? {} : { backlog }) });
|
|
473
|
+
return {
|
|
474
|
+
get addr() {
|
|
475
|
+
const a = server.address();
|
|
476
|
+
return a === null
|
|
477
|
+
? null
|
|
478
|
+
: { transport: "tcp", hostname: a.address, port: a.port };
|
|
479
|
+
},
|
|
480
|
+
settled() {
|
|
481
|
+
return settledOnce;
|
|
482
|
+
},
|
|
483
|
+
accept() {
|
|
484
|
+
if (failure !== undefined)
|
|
485
|
+
return Promise.reject(failure);
|
|
486
|
+
if (closed) {
|
|
487
|
+
return Promise.reject(codedError("ERR_SERVER_NOT_RUNNING", "listener is closed"));
|
|
488
|
+
}
|
|
489
|
+
const queued = queue.shift();
|
|
490
|
+
if (queued !== undefined) {
|
|
491
|
+
return Promise.resolve(new NodeTcpConn(queued));
|
|
492
|
+
}
|
|
493
|
+
return new Promise((resolve, reject) => {
|
|
494
|
+
waiters.push({ resolve, reject });
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
tryAccept() {
|
|
498
|
+
if (failure !== undefined)
|
|
499
|
+
throw failure;
|
|
500
|
+
if (closed) {
|
|
501
|
+
throw codedError("ERR_SERVER_NOT_RUNNING", "listener is closed");
|
|
502
|
+
}
|
|
503
|
+
const queued = queue.shift();
|
|
504
|
+
return queued === undefined ? undefined : new NodeTcpConn(queued);
|
|
505
|
+
},
|
|
506
|
+
acceptReady() {
|
|
507
|
+
return queue.length > 0 || failure !== undefined || closed;
|
|
508
|
+
},
|
|
509
|
+
waitAccept() {
|
|
510
|
+
return wakePromise;
|
|
511
|
+
},
|
|
512
|
+
close() {
|
|
513
|
+
if (closed)
|
|
514
|
+
return;
|
|
515
|
+
closed = true;
|
|
516
|
+
signal();
|
|
517
|
+
failWaiters(codedError("ERR_SERVER_NOT_RUNNING", "listener closed under a pending accept"));
|
|
518
|
+
for (const socket of queue.splice(0, queue.length)) {
|
|
519
|
+
socket.destroy(); // refuse queued-but-untaken connections
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
server.close();
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// Never listening.
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
}
|