@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,992 @@
|
|
|
1
|
+
// `wasi:sockets@0.2` — network, instance-network, tcp, tcp-create-socket,
|
|
2
|
+
// udp, udp-create-socket, ip-name-lookup: the poll-shaped 0.2 surface,
|
|
3
|
+
// served over the same node-builtins backend as the 0.3 track
|
|
4
|
+
// (sockets_platform.ts) and registered by `sockets()` alongside it.
|
|
5
|
+
//
|
|
6
|
+
// THE DRIVING PATTERN (pinned empirically from the net-probe fixture's
|
|
7
|
+
// leaf imports — what wasi-libc actually calls): every potentially-slow
|
|
8
|
+
// operation is split into non-blocking halves plus a pollable —
|
|
9
|
+
// `start-connect`/`finish-connect` looping on `would-block`,
|
|
10
|
+
// `accept` returning `would-block` until `subscribe`'s pollable is ready,
|
|
11
|
+
// datagram streams with `receive(max)`/`check-send`+`send` batches — and
|
|
12
|
+
// wasi-libc emulates POSIX blocking by `pollable.block()` (the io.ts
|
|
13
|
+
// parking kernel, A14). Socket byte I/O rides `wasi:io/streams@0.2`:
|
|
14
|
+
// wasi-libc links the NON-blocking `input-stream.read` + `subscribe`
|
|
15
|
+
// (never `blocking-read`) and `check-write`/`write`/`blocking-flush` —
|
|
16
|
+
// exactly the surfaces of io.ts's async-backed `FedInputStream` /
|
|
17
|
+
// `SinkOutputStream`, which this module mints over connections. The
|
|
18
|
+
// PARKING therefore happens in `Pollable.block`/`poll` and
|
|
19
|
+
// `blocking-flush`, all already A14-marked: 0.2 socket guests need JSPI
|
|
20
|
+
// on V8 engines, like the 0.3 track's `listen`.
|
|
21
|
+
//
|
|
22
|
+
// 0.2's `error-code` is an ENUM (bare strings — the A10 rule), with a
|
|
23
|
+
// different vocabulary than 0.3's variant: it grew `unknown`,
|
|
24
|
+
// `would-block`, `not-in-progress`, `concurrency-conflict`,
|
|
25
|
+
// `new-socket-limit` and the name-lookup codes, and it lacks
|
|
26
|
+
// `connection-broken` (mapped to `connection-reset` here) and `other`
|
|
27
|
+
// (mapped to `unknown`). `network-error-code(borrow<error>)` downcasts
|
|
28
|
+
// exactly the io errors these socket streams minted (`SocketIoError`,
|
|
29
|
+
// the filesystem-error-code pattern).
|
|
30
|
+
//
|
|
31
|
+
// Recorded divergences (the sockets.ts stances carried over, plus 0.2's
|
|
32
|
+
// own):
|
|
33
|
+
//
|
|
34
|
+
// * tcp `bind` records the address; the OS bind is DEFERRED to listen/
|
|
35
|
+
// connect (node cannot bind an unconnected socket) — so finish-bind
|
|
36
|
+
// always succeeds and bind errors surface at the deferred call, with
|
|
37
|
+
// their real codes.
|
|
38
|
+
// * udp bind IS synchronous (the dgram sync-lookup trick), so
|
|
39
|
+
// start-bind performs it and finish-bind never returns would-block.
|
|
40
|
+
// * outgoing-datagram-stream.send hands datagrams to node and counts
|
|
41
|
+
// them sent — the WIT's "sent (or queued for sending)" latitude; an
|
|
42
|
+
// async send failure surfaces on the NEXT check-send/send call.
|
|
43
|
+
// * udp connected mode uses the OS connect when the backend has it,
|
|
44
|
+
// with the provider-side source filter as a BACKSTOP (Deno's dgram
|
|
45
|
+
// compat treats connect() as default-destination only).
|
|
46
|
+
// * socket options: identical honesty to the 0.3 track (module header
|
|
47
|
+
// of sockets.ts) — tcp keep-alive enabled/idle-time and udp
|
|
48
|
+
// hop-limit/buffer-sizes are real where node has API, cached-getter
|
|
49
|
+
// where it has only a setter, `not-supported` where it has neither.
|
|
50
|
+
import { ComponentException } from "@polyengine/runtime/embedder";
|
|
51
|
+
import { FedInputStream, IoError, Pollable, SinkOutputStream } from "../io.js";
|
|
52
|
+
import { dnsLookup, listenDatagram, tcpConnect, tcpListen, } from "./sockets_platform.js";
|
|
53
|
+
import { ipHostname, isUnspecified, isValidAddressFamily, mapPlatformError, MAX_UDP_DATAGRAM_SIZE, parseNetAddr, sameSocketAddress, } from "./sockets_shared.js";
|
|
54
|
+
function err02(code, detail) {
|
|
55
|
+
return new ComponentException(code, `wasi:sockets@0.2: ${detail}`);
|
|
56
|
+
}
|
|
57
|
+
/** The 0.3 variant kinds this backend produces, onto the 0.2 enum. */
|
|
58
|
+
const KIND_TO_02 = {
|
|
59
|
+
"access-denied": "access-denied",
|
|
60
|
+
"not-supported": "not-supported",
|
|
61
|
+
"invalid-argument": "invalid-argument",
|
|
62
|
+
"out-of-memory": "out-of-memory",
|
|
63
|
+
"timeout": "timeout",
|
|
64
|
+
"invalid-state": "invalid-state",
|
|
65
|
+
"address-not-bindable": "address-not-bindable",
|
|
66
|
+
"address-in-use": "address-in-use",
|
|
67
|
+
"remote-unreachable": "remote-unreachable",
|
|
68
|
+
"connection-refused": "connection-refused",
|
|
69
|
+
"connection-reset": "connection-reset",
|
|
70
|
+
"connection-aborted": "connection-aborted",
|
|
71
|
+
"datagram-too-large": "datagram-too-large",
|
|
72
|
+
// 0.2 has no connection-broken (EPIPE): reset is the nearest truth.
|
|
73
|
+
"connection-broken": "connection-reset",
|
|
74
|
+
};
|
|
75
|
+
/** Map a platform failure to the 0.2 enum (via the shared 0.3 mapper). */
|
|
76
|
+
function toCode02(e, what) {
|
|
77
|
+
if (e instanceof ComponentException) {
|
|
78
|
+
const payload = e.payload;
|
|
79
|
+
if (typeof payload === "string")
|
|
80
|
+
return payload; // already 0.2
|
|
81
|
+
const kind = payload?.kind;
|
|
82
|
+
if (typeof kind === "string")
|
|
83
|
+
return KIND_TO_02[kind] ?? "unknown";
|
|
84
|
+
return "unknown";
|
|
85
|
+
}
|
|
86
|
+
return KIND_TO_02[mapPlatformError(e, what).payload.kind] ?? "unknown";
|
|
87
|
+
}
|
|
88
|
+
function raise02(e, what) {
|
|
89
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
90
|
+
throw err02(toCode02(e, what), `${what}: ${message}`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The io `error` resource minted by 0.2 socket STREAM failures, carrying
|
|
94
|
+
* the error-code so `network-error-code(borrow<error>)` can downcast it
|
|
95
|
+
* (the filesystem-error-code pattern; SinkOutputStream and
|
|
96
|
+
* FedInputStream preserve IoError subclasses).
|
|
97
|
+
*/
|
|
98
|
+
export class SocketIoError extends IoError {
|
|
99
|
+
code;
|
|
100
|
+
constructor(code, message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.code = code;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** `wasi:sockets/network@0.2`'s opaque capability resource. */
|
|
106
|
+
export class Network {
|
|
107
|
+
}
|
|
108
|
+
const TCP_RECEIVE_CHUNK = 16384;
|
|
109
|
+
/** check-send's fixed permit (the WIT wants a positive count; sends are
|
|
110
|
+
* handed to node immediately, so the permit never genuinely shrinks). */
|
|
111
|
+
const CHECK_SEND_PERMIT = 64n;
|
|
112
|
+
/**
|
|
113
|
+
* Build the `wasi:sockets@0.2` interfaces (module header). `onCall` is
|
|
114
|
+
* the same fragment-scoped observer the 0.3 track takes.
|
|
115
|
+
*/
|
|
116
|
+
export function sockets02(onCall) {
|
|
117
|
+
// One ambient network per fragment: `instance-network` returns it and
|
|
118
|
+
// the `network` parameters below are accepted without inspection (this
|
|
119
|
+
// host models exactly one network namespace).
|
|
120
|
+
const theNetwork = new Network();
|
|
121
|
+
const validateRemote = (family, remote, what) => {
|
|
122
|
+
if (!isValidAddressFamily(family, remote) || isUnspecified(remote) ||
|
|
123
|
+
remote.value.port === 0) {
|
|
124
|
+
throw err02("invalid-argument", `${what}: the remote address must be a specific address and non-zero ` +
|
|
125
|
+
`port in the socket's family (${family})`);
|
|
126
|
+
}
|
|
127
|
+
if (remote.kind === "ipv6" && remote.value.scopeId !== 0) {
|
|
128
|
+
throw err02("not-supported", `${what}: non-zero scope-id`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const validateLocal = (family, local, what) => {
|
|
132
|
+
if (!isValidAddressFamily(family, local)) {
|
|
133
|
+
throw err02("invalid-argument", `${what}: address family mismatch (an ${family} socket)`);
|
|
134
|
+
}
|
|
135
|
+
if (local.kind === "ipv6" && local.value.scopeId !== 0) {
|
|
136
|
+
throw err02("not-supported", `${what}: non-zero scope-id`);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/** Mint the wasi:io stream pair over a connection (module header). */
|
|
140
|
+
const connStreams = (conn) => {
|
|
141
|
+
const input = new FedInputStream((async function* () {
|
|
142
|
+
for (;;) {
|
|
143
|
+
let chunk;
|
|
144
|
+
try {
|
|
145
|
+
chunk = await conn.read(TCP_RECEIVE_CHUNK);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
throw new SocketIoError(toCode02(e, "input-stream (socket read)"), e instanceof Error ? e.message : String(e));
|
|
149
|
+
}
|
|
150
|
+
if (chunk === null)
|
|
151
|
+
return; // peer FIN: clean stream close
|
|
152
|
+
if (chunk.length > 0)
|
|
153
|
+
yield chunk;
|
|
154
|
+
}
|
|
155
|
+
})());
|
|
156
|
+
const output = new SinkOutputStream(async (chunk) => {
|
|
157
|
+
try {
|
|
158
|
+
let at = 0;
|
|
159
|
+
while (at < chunk.length)
|
|
160
|
+
at += await conn.write(chunk.subarray(at));
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
throw new SocketIoError(toCode02(e, "output-stream (socket write)"), e instanceof Error ? e.message : String(e));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return [input, output];
|
|
167
|
+
};
|
|
168
|
+
class TcpSocket02 {
|
|
169
|
+
#family;
|
|
170
|
+
#state = "unbound";
|
|
171
|
+
#localRequest;
|
|
172
|
+
#dial;
|
|
173
|
+
#listen;
|
|
174
|
+
#conn;
|
|
175
|
+
#input;
|
|
176
|
+
#output;
|
|
177
|
+
/** All accepted writes so far; shutdown(send) FINs after them. */
|
|
178
|
+
#lastWrite = Promise.resolve();
|
|
179
|
+
// The options cache — the sockets.ts honesty stances.
|
|
180
|
+
#backlog;
|
|
181
|
+
#keepAliveEnabled = false;
|
|
182
|
+
#keepAliveIdleNs = 7200000000000n;
|
|
183
|
+
constructor(family) {
|
|
184
|
+
this.#family = family;
|
|
185
|
+
}
|
|
186
|
+
/** An accepted connection, already `connected`. */
|
|
187
|
+
static accepted(family, conn) {
|
|
188
|
+
const socket = new TcpSocket02(family);
|
|
189
|
+
socket.#conn = conn;
|
|
190
|
+
socket.#state = "connected";
|
|
191
|
+
return socket;
|
|
192
|
+
}
|
|
193
|
+
/** `bind` records; the OS bind is deferred (module header). */
|
|
194
|
+
startBind(_network, localAddress) {
|
|
195
|
+
onCall("tcp-socket.start-bind");
|
|
196
|
+
if (this.#state !== "unbound") {
|
|
197
|
+
throw err02("invalid-state", `tcp-socket.start-bind: not bindable from '${this.#state}'`);
|
|
198
|
+
}
|
|
199
|
+
validateLocal(this.#family, localAddress, "tcp-socket.start-bind");
|
|
200
|
+
this.#localRequest = localAddress;
|
|
201
|
+
this.#state = "bind-in-progress";
|
|
202
|
+
}
|
|
203
|
+
finishBind() {
|
|
204
|
+
onCall("tcp-socket.finish-bind");
|
|
205
|
+
if (this.#state !== "bind-in-progress") {
|
|
206
|
+
throw err02("not-in-progress", "tcp-socket.finish-bind: no bind in progress");
|
|
207
|
+
}
|
|
208
|
+
this.#state = "bound"; // recorded; deferred to listen/connect (header)
|
|
209
|
+
}
|
|
210
|
+
startConnect(_network, remoteAddress) {
|
|
211
|
+
onCall("tcp-socket.start-connect");
|
|
212
|
+
if (this.#state !== "unbound" && this.#state !== "bound") {
|
|
213
|
+
throw err02("invalid-state", `tcp-socket.start-connect: not connectable from '${this.#state}'`);
|
|
214
|
+
}
|
|
215
|
+
validateRemote(this.#family, remoteAddress, "tcp-socket.start-connect");
|
|
216
|
+
const connect = tcpConnect();
|
|
217
|
+
if (connect === undefined) {
|
|
218
|
+
throw err02("not-supported", "tcp-socket.start-connect: no TCP backend (no node:net)");
|
|
219
|
+
}
|
|
220
|
+
const local = this.#localRequest;
|
|
221
|
+
const dial = { done: false, wait: Promise.resolve() };
|
|
222
|
+
dial.wait = connect({
|
|
223
|
+
transport: "tcp",
|
|
224
|
+
hostname: ipHostname(remoteAddress),
|
|
225
|
+
port: remoteAddress.value.port,
|
|
226
|
+
...(local === undefined ? {} : {
|
|
227
|
+
localHostname: ipHostname(local),
|
|
228
|
+
localPort: local.value.port,
|
|
229
|
+
}),
|
|
230
|
+
}).then((conn) => {
|
|
231
|
+
dial.conn = conn;
|
|
232
|
+
dial.done = true;
|
|
233
|
+
// Dropped mid-dial: nothing will ever take this conn.
|
|
234
|
+
if (this.#state !== "connect-in-progress")
|
|
235
|
+
conn.close();
|
|
236
|
+
}, (e) => {
|
|
237
|
+
dial.error = e;
|
|
238
|
+
dial.done = true;
|
|
239
|
+
});
|
|
240
|
+
this.#dial = dial;
|
|
241
|
+
this.#state = "connect-in-progress";
|
|
242
|
+
}
|
|
243
|
+
finishConnect() {
|
|
244
|
+
onCall("tcp-socket.finish-connect");
|
|
245
|
+
const dial = this.#dial;
|
|
246
|
+
if (this.#state !== "connect-in-progress" || dial === undefined) {
|
|
247
|
+
throw err02("not-in-progress", "tcp-socket.finish-connect: no connect in progress");
|
|
248
|
+
}
|
|
249
|
+
if (!dial.done) {
|
|
250
|
+
throw err02("would-block", "tcp-socket.finish-connect: the dial has not settled");
|
|
251
|
+
}
|
|
252
|
+
if (dial.error !== undefined || dial.conn === undefined) {
|
|
253
|
+
// "After a failed connection attempt ... the only valid action
|
|
254
|
+
// left is to drop".
|
|
255
|
+
this.#state = "closed";
|
|
256
|
+
this.#dial = undefined;
|
|
257
|
+
raise02(dial.error, "tcp-socket.finish-connect");
|
|
258
|
+
}
|
|
259
|
+
this.#conn = dial.conn;
|
|
260
|
+
this.#dial = undefined;
|
|
261
|
+
this.#state = "connected";
|
|
262
|
+
this.#applyKeepAlive();
|
|
263
|
+
const [input, output] = this.#mintStreams(this.#conn);
|
|
264
|
+
return [input, output];
|
|
265
|
+
}
|
|
266
|
+
startListen() {
|
|
267
|
+
onCall("tcp-socket.start-listen");
|
|
268
|
+
if (this.#state !== "unbound" && this.#state !== "bound") {
|
|
269
|
+
throw err02("invalid-state", `tcp-socket.start-listen: not listenable from '${this.#state}'`);
|
|
270
|
+
}
|
|
271
|
+
const listen = tcpListen();
|
|
272
|
+
if (listen === undefined) {
|
|
273
|
+
throw err02("not-supported", "tcp-socket.start-listen: no TCP backend (no node:net)");
|
|
274
|
+
}
|
|
275
|
+
const local = this.#localRequest ?? wildcard(this.#family);
|
|
276
|
+
const listener = listen({
|
|
277
|
+
transport: "tcp",
|
|
278
|
+
hostname: ipHostname(local),
|
|
279
|
+
port: local.value.port,
|
|
280
|
+
...(this.#backlog === undefined ? {} : { backlog: this.#backlog }),
|
|
281
|
+
});
|
|
282
|
+
const state = { listener, settled: false, wait: Promise.resolve() };
|
|
283
|
+
state.wait = listener.settled().then(() => {
|
|
284
|
+
state.settled = true;
|
|
285
|
+
}, (e) => {
|
|
286
|
+
state.error = e;
|
|
287
|
+
state.settled = true;
|
|
288
|
+
});
|
|
289
|
+
this.#listen = state;
|
|
290
|
+
this.#state = "listen-in-progress";
|
|
291
|
+
}
|
|
292
|
+
finishListen() {
|
|
293
|
+
onCall("tcp-socket.finish-listen");
|
|
294
|
+
const state = this.#listen;
|
|
295
|
+
if (this.#state !== "listen-in-progress" || state === undefined) {
|
|
296
|
+
throw err02("not-in-progress", "tcp-socket.finish-listen: no listen in progress");
|
|
297
|
+
}
|
|
298
|
+
if (!state.settled) {
|
|
299
|
+
throw err02("would-block", "tcp-socket.finish-listen: the OS bind has not settled");
|
|
300
|
+
}
|
|
301
|
+
if (state.error !== undefined) {
|
|
302
|
+
state.listener.close();
|
|
303
|
+
this.#listen = undefined;
|
|
304
|
+
this.#state = "closed";
|
|
305
|
+
raise02(state.error, "tcp-socket.finish-listen");
|
|
306
|
+
}
|
|
307
|
+
this.#state = "listening";
|
|
308
|
+
}
|
|
309
|
+
accept() {
|
|
310
|
+
onCall("tcp-socket.accept");
|
|
311
|
+
const state = this.#listen;
|
|
312
|
+
if (this.#state !== "listening" || state === undefined) {
|
|
313
|
+
throw err02("invalid-state", "tcp-socket.accept: the socket is not listening");
|
|
314
|
+
}
|
|
315
|
+
let conn;
|
|
316
|
+
try {
|
|
317
|
+
conn = state.listener.tryAccept === undefined
|
|
318
|
+
? undefined
|
|
319
|
+
: state.listener.tryAccept();
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
raise02(e, "tcp-socket.accept");
|
|
323
|
+
}
|
|
324
|
+
if (conn === undefined) {
|
|
325
|
+
throw err02("would-block", "tcp-socket.accept: no pending connections");
|
|
326
|
+
}
|
|
327
|
+
const socket = TcpSocket02.accepted(this.#family, conn);
|
|
328
|
+
const [input, output] = socket.#mintStreams(conn);
|
|
329
|
+
return [socket, input, output];
|
|
330
|
+
}
|
|
331
|
+
localAddress() {
|
|
332
|
+
onCall("tcp-socket.local-address");
|
|
333
|
+
if (this.#state === "listening" && this.#listen !== undefined) {
|
|
334
|
+
const addr = this.#listen.listener.addr;
|
|
335
|
+
if (addr !== null)
|
|
336
|
+
return parseNetAddr(addr);
|
|
337
|
+
}
|
|
338
|
+
if (this.#conn !== undefined)
|
|
339
|
+
return parseNetAddr(this.#conn.localAddr);
|
|
340
|
+
// 0.2 pins bound-but-not-yet-realized: the deferred bind means the
|
|
341
|
+
// recorded address (port possibly still 0) is the honest answer.
|
|
342
|
+
if (this.#state === "bound" && this.#localRequest !== undefined) {
|
|
343
|
+
return this.#localRequest;
|
|
344
|
+
}
|
|
345
|
+
throw err02("invalid-state", "tcp-socket.local-address: the socket is not bound");
|
|
346
|
+
}
|
|
347
|
+
remoteAddress() {
|
|
348
|
+
onCall("tcp-socket.remote-address");
|
|
349
|
+
if (this.#state !== "connected" || this.#conn === undefined) {
|
|
350
|
+
throw err02("invalid-state", "tcp-socket.remote-address: not connected");
|
|
351
|
+
}
|
|
352
|
+
return parseNetAddr(this.#conn.remoteAddr);
|
|
353
|
+
}
|
|
354
|
+
isListening() {
|
|
355
|
+
onCall("tcp-socket.is-listening");
|
|
356
|
+
return this.#state === "listening";
|
|
357
|
+
}
|
|
358
|
+
addressFamily() {
|
|
359
|
+
onCall("tcp-socket.address-family");
|
|
360
|
+
return this.#family;
|
|
361
|
+
}
|
|
362
|
+
setListenBacklogSize(value) {
|
|
363
|
+
onCall("tcp-socket.set-listen-backlog-size");
|
|
364
|
+
if (value === 0n) {
|
|
365
|
+
throw err02("invalid-argument", "tcp-socket.set-listen-backlog-size: zero");
|
|
366
|
+
}
|
|
367
|
+
if (this.#state === "listening" || this.#state === "listen-in-progress") {
|
|
368
|
+
throw err02("not-supported", "tcp-socket.set-listen-backlog-size: node cannot re-listen");
|
|
369
|
+
}
|
|
370
|
+
if (this.#state !== "unbound" && this.#state !== "bound") {
|
|
371
|
+
throw err02("invalid-state", `tcp-socket.set-listen-backlog-size: not settable in '${this.#state}'`);
|
|
372
|
+
}
|
|
373
|
+
this.#backlog = Number(value > 0x7fffffffn ? 0x7fffffffn : value);
|
|
374
|
+
}
|
|
375
|
+
keepAliveEnabled() {
|
|
376
|
+
onCall("tcp-socket.keep-alive-enabled");
|
|
377
|
+
return this.#keepAliveEnabled;
|
|
378
|
+
}
|
|
379
|
+
setKeepAliveEnabled(value) {
|
|
380
|
+
onCall("tcp-socket.set-keep-alive-enabled");
|
|
381
|
+
this.#keepAliveEnabled = value;
|
|
382
|
+
this.#applyKeepAlive();
|
|
383
|
+
}
|
|
384
|
+
keepAliveIdleTime() {
|
|
385
|
+
onCall("tcp-socket.keep-alive-idle-time");
|
|
386
|
+
return this.#keepAliveIdleNs;
|
|
387
|
+
}
|
|
388
|
+
setKeepAliveIdleTime(value) {
|
|
389
|
+
onCall("tcp-socket.set-keep-alive-idle-time");
|
|
390
|
+
if (value < 1n) {
|
|
391
|
+
throw err02("invalid-argument", "tcp-socket.set-keep-alive-idle-time: zero");
|
|
392
|
+
}
|
|
393
|
+
this.#keepAliveIdleNs = value;
|
|
394
|
+
this.#applyKeepAlive();
|
|
395
|
+
}
|
|
396
|
+
// No node:net API (the sockets.ts honesty stance):
|
|
397
|
+
keepAliveInterval() {
|
|
398
|
+
onCall("tcp-socket.keep-alive-interval");
|
|
399
|
+
throw this.#noOption("keep-alive-interval (TCP_KEEPINTVL)");
|
|
400
|
+
}
|
|
401
|
+
setKeepAliveInterval(_v) {
|
|
402
|
+
onCall("tcp-socket.set-keep-alive-interval");
|
|
403
|
+
throw this.#noOption("keep-alive-interval (TCP_KEEPINTVL)");
|
|
404
|
+
}
|
|
405
|
+
keepAliveCount() {
|
|
406
|
+
onCall("tcp-socket.keep-alive-count");
|
|
407
|
+
throw this.#noOption("keep-alive-count (TCP_KEEPCNT)");
|
|
408
|
+
}
|
|
409
|
+
setKeepAliveCount(_v) {
|
|
410
|
+
onCall("tcp-socket.set-keep-alive-count");
|
|
411
|
+
throw this.#noOption("keep-alive-count (TCP_KEEPCNT)");
|
|
412
|
+
}
|
|
413
|
+
hopLimit() {
|
|
414
|
+
onCall("tcp-socket.hop-limit");
|
|
415
|
+
throw this.#noOption("hop-limit (IP_TTL)");
|
|
416
|
+
}
|
|
417
|
+
setHopLimit(_v) {
|
|
418
|
+
onCall("tcp-socket.set-hop-limit");
|
|
419
|
+
throw this.#noOption("hop-limit (IP_TTL)");
|
|
420
|
+
}
|
|
421
|
+
receiveBufferSize() {
|
|
422
|
+
onCall("tcp-socket.receive-buffer-size");
|
|
423
|
+
throw this.#noOption("receive-buffer-size (SO_RCVBUF)");
|
|
424
|
+
}
|
|
425
|
+
setReceiveBufferSize(_v) {
|
|
426
|
+
onCall("tcp-socket.set-receive-buffer-size");
|
|
427
|
+
throw this.#noOption("receive-buffer-size (SO_RCVBUF)");
|
|
428
|
+
}
|
|
429
|
+
sendBufferSize() {
|
|
430
|
+
onCall("tcp-socket.send-buffer-size");
|
|
431
|
+
throw this.#noOption("send-buffer-size (SO_SNDBUF)");
|
|
432
|
+
}
|
|
433
|
+
setSendBufferSize(_v) {
|
|
434
|
+
onCall("tcp-socket.set-send-buffer-size");
|
|
435
|
+
throw this.#noOption("send-buffer-size (SO_SNDBUF)");
|
|
436
|
+
}
|
|
437
|
+
/** Readiness for the CURRENT pending operation (module header). */
|
|
438
|
+
subscribe() {
|
|
439
|
+
onCall("tcp-socket.subscribe");
|
|
440
|
+
const dial = this.#dial;
|
|
441
|
+
if (this.#state === "connect-in-progress" && dial !== undefined) {
|
|
442
|
+
return new Pollable(() => dial.done, () => dial.wait);
|
|
443
|
+
}
|
|
444
|
+
const listen = this.#listen;
|
|
445
|
+
if (this.#state === "listen-in-progress" && listen !== undefined) {
|
|
446
|
+
return new Pollable(() => listen.settled, () => listen.wait);
|
|
447
|
+
}
|
|
448
|
+
if (this.#state === "listening" && listen !== undefined) {
|
|
449
|
+
const l = listen.listener;
|
|
450
|
+
return new Pollable(() => l.acceptReady === undefined ? true : l.acceptReady(), () => l.waitAccept === undefined ? Promise.resolve() : l.waitAccept());
|
|
451
|
+
}
|
|
452
|
+
return new Pollable(); // no pending operation: ready
|
|
453
|
+
}
|
|
454
|
+
shutdown(shutdownType) {
|
|
455
|
+
onCall("tcp-socket.shutdown");
|
|
456
|
+
if (this.#state !== "connected" || this.#conn === undefined) {
|
|
457
|
+
throw err02("invalid-state", "tcp-socket.shutdown: the socket is not connected");
|
|
458
|
+
}
|
|
459
|
+
const conn = this.#conn;
|
|
460
|
+
if (shutdownType === "receive" || shutdownType === "both") {
|
|
461
|
+
this.#input?.[Symbol.dispose]();
|
|
462
|
+
}
|
|
463
|
+
if (shutdownType === "send" || shutdownType === "both") {
|
|
464
|
+
// FIN after everything the output stream already accepted (the
|
|
465
|
+
// kernel analogue: shutdown(SHUT_WR) follows queued data out).
|
|
466
|
+
void this.#lastWrite.then(() => conn.closeWrite()).catch(() => {
|
|
467
|
+
// The FIN lost a race with a reset; the streams report it.
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
#mintStreams(conn) {
|
|
472
|
+
const [input, rawOutput] = connStreams(conn);
|
|
473
|
+
// Interpose on the sink serialization so shutdown(send) can chain
|
|
474
|
+
// the FIN after the last accepted write.
|
|
475
|
+
const output = new SinkOutputStream((chunk) => {
|
|
476
|
+
const p = (async () => {
|
|
477
|
+
try {
|
|
478
|
+
let at = 0;
|
|
479
|
+
while (at < chunk.length)
|
|
480
|
+
at += await conn.write(chunk.subarray(at));
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
throw new SocketIoError(toCode02(e, "output-stream (socket write)"), e instanceof Error ? e.message : String(e));
|
|
484
|
+
}
|
|
485
|
+
})();
|
|
486
|
+
this.#lastWrite = p.catch(() => { });
|
|
487
|
+
return p;
|
|
488
|
+
});
|
|
489
|
+
rawOutput[Symbol.dispose]();
|
|
490
|
+
this.#input = input;
|
|
491
|
+
this.#output = output;
|
|
492
|
+
return [input, output];
|
|
493
|
+
}
|
|
494
|
+
#noOption(what) {
|
|
495
|
+
return err02("not-supported", `tcp-socket: node:net exposes no ${what}`);
|
|
496
|
+
}
|
|
497
|
+
#applyKeepAlive() {
|
|
498
|
+
const conn = this.#conn;
|
|
499
|
+
if (this.#state !== "connected" || conn === undefined)
|
|
500
|
+
return;
|
|
501
|
+
if (conn.setKeepAlive === undefined) {
|
|
502
|
+
throw err02("not-supported", "tcp-socket: no keep-alive control on this backend");
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
conn.setKeepAlive(this.#keepAliveEnabled, Number(this.#keepAliveIdleNs / 1000000n));
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
raise02(e, "tcp-socket (applying keep-alive)");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
[Symbol.dispose]() {
|
|
512
|
+
// The wasi:io streams hold their own references to the conn's byte
|
|
513
|
+
// channels in wasi-libc's hands; 0.2 sockets close with the handle
|
|
514
|
+
// (the streams then observe reset/closed) — matching wasmtime,
|
|
515
|
+
// where dropping the socket closes the fd.
|
|
516
|
+
this.#state = "closed";
|
|
517
|
+
this.#input?.[Symbol.dispose]();
|
|
518
|
+
this.#output?.[Symbol.dispose]();
|
|
519
|
+
try {
|
|
520
|
+
this.#conn?.close();
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Already closed.
|
|
524
|
+
}
|
|
525
|
+
this.#conn = undefined;
|
|
526
|
+
const listener = this.#listen?.listener;
|
|
527
|
+
this.#listen = undefined;
|
|
528
|
+
if (listener !== undefined) {
|
|
529
|
+
try {
|
|
530
|
+
listener.close();
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
// Already closed.
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const createTcpSocket = (addressFamily) => {
|
|
539
|
+
onCall("tcp-create-socket.create-tcp-socket");
|
|
540
|
+
if (tcpConnect() === undefined) {
|
|
541
|
+
throw err02("not-supported", "create-tcp-socket: no TCP backend (no node:net)");
|
|
542
|
+
}
|
|
543
|
+
return new TcpSocket02(addressFamily);
|
|
544
|
+
};
|
|
545
|
+
class UdpSocket02 {
|
|
546
|
+
#family;
|
|
547
|
+
#state = "unbound";
|
|
548
|
+
#conn;
|
|
549
|
+
#streams;
|
|
550
|
+
#generation = 0;
|
|
551
|
+
#hopLimit;
|
|
552
|
+
#recvBuffer;
|
|
553
|
+
#sendBuffer;
|
|
554
|
+
constructor(family) {
|
|
555
|
+
this.#family = family;
|
|
556
|
+
}
|
|
557
|
+
/** udp bind is synchronous here (module header): start performs it. */
|
|
558
|
+
startBind(_network, localAddress) {
|
|
559
|
+
onCall("udp-socket.start-bind");
|
|
560
|
+
if (this.#state !== "unbound") {
|
|
561
|
+
throw err02("invalid-state", `udp-socket.start-bind: not bindable from '${this.#state}'`);
|
|
562
|
+
}
|
|
563
|
+
validateLocal(this.#family, localAddress, "udp-socket.start-bind");
|
|
564
|
+
const listen = listenDatagram();
|
|
565
|
+
if (listen === undefined) {
|
|
566
|
+
throw err02("not-supported", "udp-socket.start-bind: no datagram backend (no node:dgram)");
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
this.#conn = listen({
|
|
570
|
+
transport: "udp",
|
|
571
|
+
hostname: ipHostname(localAddress),
|
|
572
|
+
port: localAddress.value.port,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
catch (e) {
|
|
576
|
+
raise02(e, "udp-socket.start-bind");
|
|
577
|
+
}
|
|
578
|
+
this.#applyCachedOptions();
|
|
579
|
+
this.#state = "bind-in-progress";
|
|
580
|
+
}
|
|
581
|
+
finishBind() {
|
|
582
|
+
onCall("udp-socket.finish-bind");
|
|
583
|
+
if (this.#state !== "bind-in-progress") {
|
|
584
|
+
throw err02("not-in-progress", "udp-socket.finish-bind: no bind in progress");
|
|
585
|
+
}
|
|
586
|
+
this.#state = "bound";
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* `stream(remote?)` — replaces any previous stream pair (they turn
|
|
590
|
+
* `invalid-state`); `some` = connected mode (OS connect when the
|
|
591
|
+
* backend has it, filter backstop either way — module header).
|
|
592
|
+
* SUSPENDING via the registered mark relay is NOT needed: node's
|
|
593
|
+
* dgram connect settles via callback, so this parks one tick through
|
|
594
|
+
* the runtime's thenable acceptance on... no — 0.2 `stream` is a
|
|
595
|
+
* sync WIT func, so the connect is fired and the streams are handed
|
|
596
|
+
* out immediately; a connect failure surfaces on the first
|
|
597
|
+
* send/receive (recorded divergence — POSIX UDP connect failures are
|
|
598
|
+
* mostly lazy anyway).
|
|
599
|
+
*/
|
|
600
|
+
stream(remoteAddress) {
|
|
601
|
+
onCall("udp-socket.stream");
|
|
602
|
+
if (this.#state !== "bound" || this.#conn === undefined) {
|
|
603
|
+
throw err02("invalid-state", "udp-socket.stream: the socket is not bound");
|
|
604
|
+
}
|
|
605
|
+
const conn = this.#conn;
|
|
606
|
+
if (remoteAddress !== undefined) {
|
|
607
|
+
validateRemote(this.#family, remoteAddress, "udp-socket.stream");
|
|
608
|
+
}
|
|
609
|
+
const wasConnected = this.#streams?.remote !== undefined;
|
|
610
|
+
this.#generation++;
|
|
611
|
+
const streams = { generation: this.#generation, remote: remoteAddress };
|
|
612
|
+
this.#streams = streams;
|
|
613
|
+
// OS-level (dis)connect, fire-and-forget: failures surface on the
|
|
614
|
+
// first datagram op (doc comment above).
|
|
615
|
+
if (remoteAddress !== undefined && conn.connect !== undefined) {
|
|
616
|
+
void conn.connect({
|
|
617
|
+
transport: "udp",
|
|
618
|
+
hostname: ipHostname(remoteAddress),
|
|
619
|
+
port: remoteAddress.value.port,
|
|
620
|
+
}).catch(() => {
|
|
621
|
+
// The filter backstop still enforces connected-mode semantics.
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
else if (remoteAddress === undefined && wasConnected) {
|
|
625
|
+
try {
|
|
626
|
+
conn.disconnect?.();
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Not connected at the OS level (compat backends).
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const live = (gen) => this.#streams?.generation === gen && this.#state === "bound";
|
|
633
|
+
return [
|
|
634
|
+
new IncomingDatagramStream02(conn, streams, live),
|
|
635
|
+
new OutgoingDatagramStream02(conn, streams, live, this.#family),
|
|
636
|
+
];
|
|
637
|
+
}
|
|
638
|
+
localAddress() {
|
|
639
|
+
onCall("udp-socket.local-address");
|
|
640
|
+
if (this.#conn === undefined) {
|
|
641
|
+
throw err02("invalid-state", "udp-socket.local-address: the socket is not bound");
|
|
642
|
+
}
|
|
643
|
+
return parseNetAddr(this.#conn.addr);
|
|
644
|
+
}
|
|
645
|
+
remoteAddress() {
|
|
646
|
+
onCall("udp-socket.remote-address");
|
|
647
|
+
const remote = this.#streams?.remote;
|
|
648
|
+
if (remote === undefined) {
|
|
649
|
+
throw err02("invalid-state", "udp-socket.remote-address: the socket is not connected");
|
|
650
|
+
}
|
|
651
|
+
return remote;
|
|
652
|
+
}
|
|
653
|
+
addressFamily() {
|
|
654
|
+
onCall("udp-socket.address-family");
|
|
655
|
+
return this.#family;
|
|
656
|
+
}
|
|
657
|
+
unicastHopLimit() {
|
|
658
|
+
onCall("udp-socket.unicast-hop-limit");
|
|
659
|
+
return this.#hopLimit ?? 64; // the documented default (sockets.ts stance)
|
|
660
|
+
}
|
|
661
|
+
setUnicastHopLimit(value) {
|
|
662
|
+
onCall("udp-socket.set-unicast-hop-limit");
|
|
663
|
+
if (value < 1) {
|
|
664
|
+
throw err02("invalid-argument", "udp-socket.set-unicast-hop-limit: below 1");
|
|
665
|
+
}
|
|
666
|
+
this.#hopLimit = value;
|
|
667
|
+
this.#applyCachedOptions();
|
|
668
|
+
}
|
|
669
|
+
receiveBufferSize() {
|
|
670
|
+
onCall("udp-socket.receive-buffer-size");
|
|
671
|
+
return this.#bufferSize("receive", this.#recvBuffer, this.#conn?.getRecvBufferSize);
|
|
672
|
+
}
|
|
673
|
+
setReceiveBufferSize(value) {
|
|
674
|
+
onCall("udp-socket.set-receive-buffer-size");
|
|
675
|
+
if (value === 0n) {
|
|
676
|
+
throw err02("invalid-argument", "udp-socket.set-receive-buffer-size: zero");
|
|
677
|
+
}
|
|
678
|
+
this.#recvBuffer = value;
|
|
679
|
+
this.#applyCachedOptions();
|
|
680
|
+
}
|
|
681
|
+
sendBufferSize() {
|
|
682
|
+
onCall("udp-socket.send-buffer-size");
|
|
683
|
+
return this.#bufferSize("send", this.#sendBuffer, this.#conn?.getSendBufferSize);
|
|
684
|
+
}
|
|
685
|
+
setSendBufferSize(value) {
|
|
686
|
+
onCall("udp-socket.set-send-buffer-size");
|
|
687
|
+
if (value === 0n) {
|
|
688
|
+
throw err02("invalid-argument", "udp-socket.set-send-buffer-size: zero");
|
|
689
|
+
}
|
|
690
|
+
this.#sendBuffer = value;
|
|
691
|
+
this.#applyCachedOptions();
|
|
692
|
+
}
|
|
693
|
+
subscribe() {
|
|
694
|
+
onCall("udp-socket.subscribe");
|
|
695
|
+
return new Pollable(); // binds are synchronous: never mid-operation
|
|
696
|
+
}
|
|
697
|
+
#bufferSize(which, cached, live) {
|
|
698
|
+
if (this.#conn !== undefined && live !== undefined) {
|
|
699
|
+
try {
|
|
700
|
+
return BigInt(live.call(this.#conn));
|
|
701
|
+
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
raise02(e, `udp-socket.${which}-buffer-size`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (cached !== undefined)
|
|
707
|
+
return cached;
|
|
708
|
+
throw err02("not-supported", `udp-socket.${which}-buffer-size: unknowable before bind on this host`);
|
|
709
|
+
}
|
|
710
|
+
#applyCachedOptions() {
|
|
711
|
+
const conn = this.#conn;
|
|
712
|
+
if (conn === undefined)
|
|
713
|
+
return;
|
|
714
|
+
try {
|
|
715
|
+
if (this.#hopLimit !== undefined)
|
|
716
|
+
conn.setTtl?.(this.#hopLimit);
|
|
717
|
+
if (this.#recvBuffer !== undefined)
|
|
718
|
+
conn.setRecvBufferSize?.(Number(this.#recvBuffer));
|
|
719
|
+
if (this.#sendBuffer !== undefined)
|
|
720
|
+
conn.setSendBufferSize?.(Number(this.#sendBuffer));
|
|
721
|
+
}
|
|
722
|
+
catch (e) {
|
|
723
|
+
raise02(e, "udp-socket (applying cached options)");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
[Symbol.dispose]() {
|
|
727
|
+
this.#state = "closed";
|
|
728
|
+
this.#streams = undefined;
|
|
729
|
+
const conn = this.#conn;
|
|
730
|
+
this.#conn = undefined;
|
|
731
|
+
if (conn !== undefined) {
|
|
732
|
+
try {
|
|
733
|
+
conn.close();
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
// Already closed.
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
class IncomingDatagramStream02 {
|
|
742
|
+
#conn;
|
|
743
|
+
#streams;
|
|
744
|
+
#live;
|
|
745
|
+
constructor(conn, streams, live) {
|
|
746
|
+
this.#conn = conn;
|
|
747
|
+
this.#streams = streams;
|
|
748
|
+
this.#live = live;
|
|
749
|
+
}
|
|
750
|
+
/** Non-blocking batch; empty list = nothing pending (never would-block). */
|
|
751
|
+
receive(maxResults) {
|
|
752
|
+
onCall("incoming-datagram-stream.receive");
|
|
753
|
+
if (!this.#live(this.#streams.generation)) {
|
|
754
|
+
throw err02("invalid-state", "incoming-datagram-stream.receive: stale stream");
|
|
755
|
+
}
|
|
756
|
+
const out = [];
|
|
757
|
+
const max = Number(maxResults);
|
|
758
|
+
while (out.length < max) {
|
|
759
|
+
let item;
|
|
760
|
+
try {
|
|
761
|
+
item = this.#conn.tryReceive === undefined ? undefined : this.#conn.tryReceive();
|
|
762
|
+
}
|
|
763
|
+
catch (e) {
|
|
764
|
+
raise02(e, "incoming-datagram-stream.receive");
|
|
765
|
+
}
|
|
766
|
+
if (item === undefined)
|
|
767
|
+
break;
|
|
768
|
+
const source = parseNetAddr(item[1]);
|
|
769
|
+
// The connected-mode filter backstop (module header).
|
|
770
|
+
const remote = this.#streams.remote;
|
|
771
|
+
if (remote !== undefined && !sameSocketAddress(source, remote))
|
|
772
|
+
continue;
|
|
773
|
+
out.push({ data: item[0], remoteAddress: source });
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
777
|
+
subscribe() {
|
|
778
|
+
onCall("incoming-datagram-stream.subscribe");
|
|
779
|
+
const conn = this.#conn;
|
|
780
|
+
return new Pollable(() => conn.receiveReady === undefined ? true : conn.receiveReady(), () => conn.waitReceive === undefined ? Promise.resolve() : conn.waitReceive());
|
|
781
|
+
}
|
|
782
|
+
[Symbol.dispose]() {
|
|
783
|
+
// The socket owns the OS resources; streams are views.
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
class OutgoingDatagramStream02 {
|
|
787
|
+
#conn;
|
|
788
|
+
#streams;
|
|
789
|
+
#live;
|
|
790
|
+
#family;
|
|
791
|
+
/** An async send failure, surfaced on the NEXT call (module header). */
|
|
792
|
+
#failure;
|
|
793
|
+
constructor(conn, streams, live, family) {
|
|
794
|
+
this.#conn = conn;
|
|
795
|
+
this.#streams = streams;
|
|
796
|
+
this.#live = live;
|
|
797
|
+
this.#family = family;
|
|
798
|
+
}
|
|
799
|
+
checkSend() {
|
|
800
|
+
onCall("outgoing-datagram-stream.check-send");
|
|
801
|
+
this.#checkLive("check-send");
|
|
802
|
+
return CHECK_SEND_PERMIT;
|
|
803
|
+
}
|
|
804
|
+
/** Hands each datagram to node and counts it sent — the WIT's "sent
|
|
805
|
+
* (or queued for sending)" latitude (module header). */
|
|
806
|
+
send(datagrams) {
|
|
807
|
+
onCall("outgoing-datagram-stream.send");
|
|
808
|
+
this.#checkLive("send");
|
|
809
|
+
if (BigInt(datagrams.length) > CHECK_SEND_PERMIT) {
|
|
810
|
+
// "Implementations must trap if ... more items than check-send
|
|
811
|
+
// permitted": the unbranded throw IS the trap.
|
|
812
|
+
throw new Error(`outgoing-datagram-stream.send: ${datagrams.length} datagrams exceed the check-send permit`);
|
|
813
|
+
}
|
|
814
|
+
const connected = this.#streams.remote;
|
|
815
|
+
for (const datagram of datagrams) {
|
|
816
|
+
if (datagram.data.length > MAX_UDP_DATAGRAM_SIZE) {
|
|
817
|
+
throw err02("datagram-too-large", `outgoing-datagram-stream.send: ${datagram.data.length} bytes`);
|
|
818
|
+
}
|
|
819
|
+
const remote = datagram.remoteAddress;
|
|
820
|
+
if (connected !== undefined) {
|
|
821
|
+
if (remote !== undefined && !sameSocketAddress(remote, connected)) {
|
|
822
|
+
throw err02("invalid-argument", "outgoing-datagram-stream.send: a remote-address that does not match the connected remote");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
if (remote === undefined) {
|
|
827
|
+
throw err02("invalid-argument", "outgoing-datagram-stream.send: no remote-address on an unconnected stream");
|
|
828
|
+
}
|
|
829
|
+
validateRemote(this.#family, remote, "outgoing-datagram-stream.send");
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
for (const datagram of datagrams) {
|
|
833
|
+
const dest = connected !== undefined ? undefined : {
|
|
834
|
+
transport: "udp",
|
|
835
|
+
hostname: ipHostname(datagram.remoteAddress),
|
|
836
|
+
port: datagram.remoteAddress.value.port,
|
|
837
|
+
};
|
|
838
|
+
void this.#conn.send(datagram.data, dest).catch((e) => {
|
|
839
|
+
this.#failure ??= toCode02(e, "outgoing-datagram-stream.send");
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return BigInt(datagrams.length);
|
|
843
|
+
}
|
|
844
|
+
subscribe() {
|
|
845
|
+
onCall("outgoing-datagram-stream.subscribe");
|
|
846
|
+
return new Pollable(); // the permit is always available
|
|
847
|
+
}
|
|
848
|
+
#checkLive(what) {
|
|
849
|
+
if (!this.#live(this.#streams.generation)) {
|
|
850
|
+
throw err02("invalid-state", `outgoing-datagram-stream.${what}: stale stream`);
|
|
851
|
+
}
|
|
852
|
+
if (this.#failure !== undefined) {
|
|
853
|
+
const code = this.#failure;
|
|
854
|
+
this.#failure = undefined;
|
|
855
|
+
throw err02(code, `outgoing-datagram-stream.${what}: an earlier send failed`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
[Symbol.dispose]() {
|
|
859
|
+
// The socket owns the OS resources; streams are views.
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const createUdpSocket = (addressFamily) => {
|
|
863
|
+
onCall("udp-create-socket.create-udp-socket");
|
|
864
|
+
if (listenDatagram() === undefined) {
|
|
865
|
+
throw err02("not-supported", "create-udp-socket: no datagram backend (no node:dgram)");
|
|
866
|
+
}
|
|
867
|
+
return new UdpSocket02(addressFamily);
|
|
868
|
+
};
|
|
869
|
+
// --- ip-name-lookup ------------------------------------------------------------
|
|
870
|
+
class ResolveAddressStream02 {
|
|
871
|
+
#settled = false;
|
|
872
|
+
#answers = [];
|
|
873
|
+
#error;
|
|
874
|
+
#wait;
|
|
875
|
+
constructor(query) {
|
|
876
|
+
if (typeof query !== "string") {
|
|
877
|
+
// IP literals: already answered, no resolver involved.
|
|
878
|
+
this.#settled = true;
|
|
879
|
+
this.#answers = query.literal;
|
|
880
|
+
this.#wait = Promise.resolve();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const name = query;
|
|
884
|
+
const lookup = dnsLookup();
|
|
885
|
+
if (lookup === undefined) {
|
|
886
|
+
this.#settled = true;
|
|
887
|
+
this.#error = "permanent-resolver-failure";
|
|
888
|
+
this.#wait = Promise.resolve();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
this.#wait = lookup(name).then((answers) => {
|
|
892
|
+
try {
|
|
893
|
+
this.#answers = answers.map((a) => {
|
|
894
|
+
const parsed = parseNetAddr({ hostname: a.address, port: 0 });
|
|
895
|
+
return parsed.kind === "ipv4"
|
|
896
|
+
? { kind: "ipv4", value: parsed.value.address }
|
|
897
|
+
: { kind: "ipv6", value: parsed.value.address };
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
this.#error = "unknown";
|
|
902
|
+
}
|
|
903
|
+
this.#settled = true;
|
|
904
|
+
}, (e) => {
|
|
905
|
+
const code = e?.code;
|
|
906
|
+
this.#error = code === "ENOTFOUND" || code === "EAI_NONAME" || code === "ENODATA"
|
|
907
|
+
? "name-unresolvable"
|
|
908
|
+
: code === "EAI_AGAIN" || code === "ETIMEOUT" || code === "ETIMEDOUT"
|
|
909
|
+
? "temporary-resolver-failure"
|
|
910
|
+
: code === "EACCES" || code === "EPERM"
|
|
911
|
+
? "access-denied"
|
|
912
|
+
: toCode02(e, "resolve-addresses") === "access-denied"
|
|
913
|
+
? "access-denied"
|
|
914
|
+
: "unknown";
|
|
915
|
+
this.#settled = true;
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
/** An already-answered stream (IP literals). */
|
|
919
|
+
static literal(answers) {
|
|
920
|
+
return new ResolveAddressStream02({ literal: answers });
|
|
921
|
+
}
|
|
922
|
+
resolveNextAddress() {
|
|
923
|
+
onCall("resolve-address-stream.resolve-next-address");
|
|
924
|
+
if (!this.#settled) {
|
|
925
|
+
throw err02("would-block", "resolve-next-address: the resolver has not answered");
|
|
926
|
+
}
|
|
927
|
+
if (this.#error !== undefined) {
|
|
928
|
+
throw err02(this.#error, "resolve-next-address: resolution failed");
|
|
929
|
+
}
|
|
930
|
+
return this.#answers.shift(); // undefined = none = end of stream
|
|
931
|
+
}
|
|
932
|
+
subscribe() {
|
|
933
|
+
onCall("resolve-address-stream.subscribe");
|
|
934
|
+
return new Pollable(() => this.#settled, () => this.#wait);
|
|
935
|
+
}
|
|
936
|
+
[Symbol.dispose]() { }
|
|
937
|
+
}
|
|
938
|
+
const resolveAddresses = (_network, name) => {
|
|
939
|
+
onCall("ip-name-lookup.resolve-addresses");
|
|
940
|
+
if (name.length === 0) {
|
|
941
|
+
throw err02("invalid-argument", "resolve-addresses: empty name");
|
|
942
|
+
}
|
|
943
|
+
// IP literals answer locally (wasmtime parity; no resolver involved).
|
|
944
|
+
try {
|
|
945
|
+
const bare = name.startsWith("[") ? name.slice(1, -1) : name;
|
|
946
|
+
const parsed = parseNetAddr({ hostname: bare, port: 0 });
|
|
947
|
+
return ResolveAddressStream02.literal([
|
|
948
|
+
parsed.kind === "ipv4"
|
|
949
|
+
? { kind: "ipv4", value: parsed.value.address }
|
|
950
|
+
: { kind: "ipv6", value: parsed.value.address },
|
|
951
|
+
]);
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
// Not a literal: a real name for the resolver.
|
|
955
|
+
}
|
|
956
|
+
return new ResolveAddressStream02(name);
|
|
957
|
+
};
|
|
958
|
+
return {
|
|
959
|
+
imports: {
|
|
960
|
+
"wasi:sockets/network@0.2": {
|
|
961
|
+
Network,
|
|
962
|
+
// `network-error-code(err: borrow<error>) -> option<error-code>`:
|
|
963
|
+
// downcast succeeds exactly for the io errors OUR streams minted.
|
|
964
|
+
networkErrorCode: (err) => err instanceof SocketIoError ? err.code : undefined,
|
|
965
|
+
},
|
|
966
|
+
"wasi:sockets/instance-network@0.2": {
|
|
967
|
+
instanceNetwork: () => theNetwork,
|
|
968
|
+
},
|
|
969
|
+
"wasi:sockets/tcp@0.2": { TcpSocket: TcpSocket02 },
|
|
970
|
+
"wasi:sockets/tcp-create-socket@0.2": { createTcpSocket },
|
|
971
|
+
"wasi:sockets/udp@0.2": {
|
|
972
|
+
UdpSocket: UdpSocket02,
|
|
973
|
+
IncomingDatagramStream: IncomingDatagramStream02,
|
|
974
|
+
OutgoingDatagramStream: OutgoingDatagramStream02,
|
|
975
|
+
},
|
|
976
|
+
"wasi:sockets/udp-create-socket@0.2": { createUdpSocket },
|
|
977
|
+
"wasi:sockets/ip-name-lookup@0.2": {
|
|
978
|
+
resolveAddresses,
|
|
979
|
+
ResolveAddressStream: ResolveAddressStream02,
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/** The family's wildcard address, port 0 (tcp listen's implicit bind). */
|
|
985
|
+
function wildcard(family) {
|
|
986
|
+
return family === "ipv4"
|
|
987
|
+
? { kind: "ipv4", value: { port: 0, address: [0, 0, 0, 0] } }
|
|
988
|
+
: {
|
|
989
|
+
kind: "ipv6",
|
|
990
|
+
value: { port: 0, flowInfo: 0, address: [0, 0, 0, 0, 0, 0, 0, 0], scopeId: 0 },
|
|
991
|
+
};
|
|
992
|
+
}
|