@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,969 @@
|
|
|
1
|
+
// INTERNAL: the `@0.3` track of `wasi:sockets` — `types@0.3` (UDP + TCP,
|
|
2
|
+
// client + listener) and `ip-name-lookup@0.3`. Registered by the public
|
|
3
|
+
// `sockets()` (sockets.ts, where the module-level documentation lives)
|
|
4
|
+
// alongside the poll-shaped `@0.2` track (sockets_02.ts). Vocabulary
|
|
5
|
+
// (codec, validation, error mapping, WIT types): sockets_shared.ts.
|
|
6
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
7
|
+
var useValue = arguments.length > 2;
|
|
8
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
9
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
10
|
+
}
|
|
11
|
+
return useValue ? value : void 0;
|
|
12
|
+
};
|
|
13
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
14
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
15
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
16
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
17
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
18
|
+
var _, done = false;
|
|
19
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
20
|
+
var context = {};
|
|
21
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
22
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
23
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
24
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
25
|
+
if (kind === "accessor") {
|
|
26
|
+
if (result === void 0) continue;
|
|
27
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
28
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
29
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
30
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
31
|
+
}
|
|
32
|
+
else if (_ = accept(result)) {
|
|
33
|
+
if (kind === "field") initializers.unshift(_);
|
|
34
|
+
else descriptor[key] = _;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
38
|
+
done = true;
|
|
39
|
+
};
|
|
40
|
+
import { ComponentException, Stream, suspending } from "@polyengine/runtime/embedder";
|
|
41
|
+
import { dnsLookup, listenDatagram, tcpConnect, tcpListen, } from "./sockets_platform.js";
|
|
42
|
+
import { componentError, ipHostname, isDenoError, isUnspecified, isValidAddressFamily, mapPlatformError, MAX_UDP_DATAGRAM_SIZE, parseNetAddr, RESULT_INVALID_STATE, RESULT_OK, resultErrOf, sameSocketAddress, wildcardAddress, } from "./sockets_shared.js";
|
|
43
|
+
/**
|
|
44
|
+
* Build the `@0.3` track: `wasi:sockets/types@0.3` (UDP + TCP resource
|
|
45
|
+
* classes, per-fragment so the `onCall` observer is scoped) and
|
|
46
|
+
* `wasi:sockets/ip-name-lookup@0.3`.
|
|
47
|
+
*/
|
|
48
|
+
export function sockets03(onCall) {
|
|
49
|
+
let UdpSocket = (() => {
|
|
50
|
+
let _instanceExtraInitializers = [];
|
|
51
|
+
let _connect_decorators;
|
|
52
|
+
return class UdpSocket {
|
|
53
|
+
static {
|
|
54
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
55
|
+
__esDecorate(this, null, _connect_decorators, { kind: "method", name: "connect", static: false, private: false, access: { has: obj => "connect" in obj, get: obj => obj.connect }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
56
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
57
|
+
}
|
|
58
|
+
#family = __runInitializers(this, _instanceExtraInitializers);
|
|
59
|
+
#conn;
|
|
60
|
+
/** Connected-mode remote (`connect`/`disconnect`); OS-level (kernel
|
|
61
|
+
* filters and default destination), not adapter emulation. */
|
|
62
|
+
#remote;
|
|
63
|
+
/** Cached option values, applied at (implicit) bind. */
|
|
64
|
+
#hopLimit;
|
|
65
|
+
#recvBuffer;
|
|
66
|
+
#sendBuffer;
|
|
67
|
+
constructor(family) {
|
|
68
|
+
this.#family = family;
|
|
69
|
+
}
|
|
70
|
+
static create(addressFamily) {
|
|
71
|
+
onCall("udp-socket.create");
|
|
72
|
+
if (listenDatagram() === undefined) {
|
|
73
|
+
throw componentError({ kind: "not-supported" }, "udp-socket.create: this host provides no datagram sockets (no node:dgram)");
|
|
74
|
+
}
|
|
75
|
+
return new UdpSocket(addressFamily);
|
|
76
|
+
}
|
|
77
|
+
bind(localAddress) {
|
|
78
|
+
onCall("udp-socket.bind");
|
|
79
|
+
if (this.#conn !== undefined) {
|
|
80
|
+
throw componentError({ kind: "invalid-state" }, "udp-socket.bind: already bound");
|
|
81
|
+
}
|
|
82
|
+
if (!isValidAddressFamily(this.#family, localAddress)) {
|
|
83
|
+
throw componentError({ kind: "invalid-argument" }, `udp-socket.bind: address family mismatch (an ${this.#family} socket)`);
|
|
84
|
+
}
|
|
85
|
+
if (localAddress.kind === "ipv6" && localAddress.value.scopeId !== 0) {
|
|
86
|
+
throw componentError({ kind: "not-supported" }, "udp-socket.bind: non-zero scope-id (not expressible through Deno.listenDatagram)");
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
this.#conn = this.#listen({
|
|
90
|
+
transport: "udp",
|
|
91
|
+
hostname: ipHostname(localAddress),
|
|
92
|
+
port: localAddress.value.port,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
throw mapPlatformError(e, "udp-socket.bind");
|
|
97
|
+
}
|
|
98
|
+
this.#applyCachedOptions();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* WIT (0.3.1): `connect: func(remote-address) -> result<_, error-code>`
|
|
102
|
+
* — OS-level connected mode: the kernel filters inbound datagrams to
|
|
103
|
+
* the remote and `send` needs no explicit address. An unbound socket
|
|
104
|
+
* implicitly binds to the family wildcard first (wasmtime parity).
|
|
105
|
+
*
|
|
106
|
+
* SUSPENDING (A1/A2): node's `dgram.connect` settles via callback one
|
|
107
|
+
* tick later, so this sync WIT func parks the calling frame for that
|
|
108
|
+
* tick — the same shape as tcp `listen`.
|
|
109
|
+
*/
|
|
110
|
+
async connect(remoteAddress) {
|
|
111
|
+
onCall("udp-socket.connect");
|
|
112
|
+
if (this.#remote !== undefined) {
|
|
113
|
+
throw componentError({ kind: "invalid-state" }, "udp-socket.connect: already connected (disconnect first)");
|
|
114
|
+
}
|
|
115
|
+
if (!isValidAddressFamily(this.#family, remoteAddress) ||
|
|
116
|
+
isUnspecified(remoteAddress) ||
|
|
117
|
+
remoteAddress.value.port === 0) {
|
|
118
|
+
throw componentError({ kind: "invalid-argument" }, "udp-socket.connect: the remote address must be a specific address " +
|
|
119
|
+
`and non-zero port in the socket's family (${this.#family})`);
|
|
120
|
+
}
|
|
121
|
+
if (remoteAddress.kind === "ipv6" && remoteAddress.value.scopeId !== 0) {
|
|
122
|
+
throw componentError({ kind: "not-supported" }, "udp-socket.connect: non-zero scope-id (not expressible through node addresses)");
|
|
123
|
+
}
|
|
124
|
+
if (this.#conn === undefined) {
|
|
125
|
+
try {
|
|
126
|
+
this.#conn = this.#listen({
|
|
127
|
+
transport: "udp",
|
|
128
|
+
hostname: this.#family === "ipv4" ? "0.0.0.0" : "::",
|
|
129
|
+
port: 0,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
throw mapPlatformError(e, "udp-socket.connect (implicit bind)");
|
|
134
|
+
}
|
|
135
|
+
this.#applyCachedOptions();
|
|
136
|
+
}
|
|
137
|
+
if (this.#conn.connect === undefined) {
|
|
138
|
+
throw componentError({ kind: "not-supported" }, "udp-socket.connect: this host's datagram backend has no connected mode");
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await this.#conn.connect({
|
|
142
|
+
transport: "udp",
|
|
143
|
+
hostname: ipHostname(remoteAddress),
|
|
144
|
+
port: remoteAddress.value.port,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
throw mapPlatformError(e, "udp-socket.connect");
|
|
149
|
+
}
|
|
150
|
+
this.#remote = remoteAddress;
|
|
151
|
+
}
|
|
152
|
+
disconnect() {
|
|
153
|
+
onCall("udp-socket.disconnect");
|
|
154
|
+
if (this.#remote === undefined || this.#conn === undefined) {
|
|
155
|
+
throw componentError({ kind: "invalid-state" }, "udp-socket.disconnect: the socket is not connected");
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
this.#conn.disconnect?.();
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
throw mapPlatformError(e, "udp-socket.disconnect");
|
|
162
|
+
}
|
|
163
|
+
this.#remote = undefined;
|
|
164
|
+
}
|
|
165
|
+
async send(data, remoteAddress) {
|
|
166
|
+
onCall("udp-socket.send");
|
|
167
|
+
if (data.length > MAX_UDP_DATAGRAM_SIZE) {
|
|
168
|
+
throw componentError({ kind: "datagram-too-large" }, `udp-socket.send: ${data.length} bytes exceeds the ${MAX_UDP_DATAGRAM_SIZE}-byte ceiling`);
|
|
169
|
+
}
|
|
170
|
+
// Connected mode (0.3.1): an omitted remote sends to the connected
|
|
171
|
+
// address (the kernel's default destination); a PRESENT remote on a
|
|
172
|
+
// connected socket is invalid-argument (wasmtime parity — node's
|
|
173
|
+
// dgram would raise ERR_SOCKET_DGRAM_IS_CONNECTED anyway). On an
|
|
174
|
+
// unconnected socket an omitted remote has no destination (POSIX
|
|
175
|
+
// EDESTADDRREQ).
|
|
176
|
+
if (remoteAddress === undefined) {
|
|
177
|
+
if (this.#remote === undefined) {
|
|
178
|
+
throw componentError({ kind: "invalid-argument" }, "udp-socket.send: no remote-address, and the socket is not connected");
|
|
179
|
+
}
|
|
180
|
+
if (this.#conn === undefined) {
|
|
181
|
+
throw componentError({ kind: "invalid-state" }, "udp-socket.send: connected but unbound (unreachable)");
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const sent = await this.#conn.send(data);
|
|
185
|
+
if (sent !== data.length) {
|
|
186
|
+
throw componentError({ kind: "other", value: `partial send: ${sent} of ${data.length} bytes` }, `udp-socket.send: partial send: ${sent} of ${data.length} bytes`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
throw mapPlatformError(e, "udp-socket.send");
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (this.#remote !== undefined) {
|
|
195
|
+
throw componentError({ kind: "invalid-argument" }, "udp-socket.send: an explicit remote-address on a connected socket");
|
|
196
|
+
}
|
|
197
|
+
if (this.#conn === undefined) {
|
|
198
|
+
// "If the socket has not been explicitly bound, it will be implicitly
|
|
199
|
+
// bound to a random free port" — the wildcard bind wasmtime performs.
|
|
200
|
+
try {
|
|
201
|
+
this.#conn = this.#listen({
|
|
202
|
+
transport: "udp",
|
|
203
|
+
hostname: this.#family === "ipv4" ? "0.0.0.0" : "::",
|
|
204
|
+
port: 0,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
throw mapPlatformError(e, "udp-socket.send (implicit bind)");
|
|
209
|
+
}
|
|
210
|
+
this.#applyCachedOptions();
|
|
211
|
+
}
|
|
212
|
+
if (!isValidAddressFamily(this.#family, remoteAddress) ||
|
|
213
|
+
isUnspecified(remoteAddress) ||
|
|
214
|
+
remoteAddress.value.port === 0) {
|
|
215
|
+
throw componentError({ kind: "invalid-argument" }, "udp-socket.send: the remote address must be a specific address and " +
|
|
216
|
+
`non-zero port in the socket's family (${this.#family})`);
|
|
217
|
+
}
|
|
218
|
+
if (remoteAddress.kind === "ipv6" && remoteAddress.value.scopeId !== 0) {
|
|
219
|
+
throw componentError({ kind: "not-supported" }, "udp-socket.send: non-zero scope-id (not expressible through Deno addresses)");
|
|
220
|
+
}
|
|
221
|
+
let sent;
|
|
222
|
+
try {
|
|
223
|
+
sent = await this.#conn.send(data, {
|
|
224
|
+
transport: "udp",
|
|
225
|
+
hostname: ipHostname(remoteAddress),
|
|
226
|
+
port: remoteAddress.value.port,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
throw mapPlatformError(e, "udp-socket.send");
|
|
231
|
+
}
|
|
232
|
+
if (sent !== data.length) {
|
|
233
|
+
throw componentError({ kind: "other", value: `partial send: ${sent} of ${data.length} bytes` }, `udp-socket.send: partial send: ${sent} of ${data.length} bytes`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async receive() {
|
|
237
|
+
onCall("udp-socket.receive");
|
|
238
|
+
if (this.#conn === undefined) {
|
|
239
|
+
throw componentError({ kind: "invalid-state" }, "udp-socket.receive: the socket is not bound");
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
for (;;) {
|
|
243
|
+
// Each datagram arrives as its own exactly-sized buffer; nothing
|
|
244
|
+
// the OS delivers is ever truncated (whole-datagram semantics).
|
|
245
|
+
const [payload, from] = await this.#conn.receive();
|
|
246
|
+
const source = parseNetAddr(from);
|
|
247
|
+
// The connected-mode filter, as a BACKSTOP over the OS's: the
|
|
248
|
+
// WIT pins "only receive datagrams from that address", the
|
|
249
|
+
// kernel filters on real node, but Deno's dgram compat treats
|
|
250
|
+
// connect() as a default destination only — so non-matching
|
|
251
|
+
// sources are dropped here either way (matching what a kernel
|
|
252
|
+
// filter would have done silently).
|
|
253
|
+
if (this.#remote === undefined || sameSocketAddress(source, this.#remote)) {
|
|
254
|
+
return [payload, source];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
throw mapPlatformError(e, "udp-socket.receive");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
getLocalAddress() {
|
|
263
|
+
onCall("udp-socket.get-local-address");
|
|
264
|
+
if (this.#conn === undefined) {
|
|
265
|
+
throw componentError({ kind: "invalid-state" }, "udp-socket.get-local-address: the socket is not bound");
|
|
266
|
+
}
|
|
267
|
+
return parseNetAddr(this.#conn.addr);
|
|
268
|
+
}
|
|
269
|
+
getRemoteAddress() {
|
|
270
|
+
onCall("udp-socket.get-remote-address");
|
|
271
|
+
if (this.#remote === undefined) {
|
|
272
|
+
throw componentError({ kind: "invalid-state" }, "udp-socket.get-remote-address: the socket is not connected");
|
|
273
|
+
}
|
|
274
|
+
return this.#remote;
|
|
275
|
+
}
|
|
276
|
+
getAddressFamily() {
|
|
277
|
+
onCall("udp-socket.get-address-family");
|
|
278
|
+
return this.#family;
|
|
279
|
+
}
|
|
280
|
+
/** Stored-value getter (documented default 64, the common OS default):
|
|
281
|
+
* node exposes a setter (`setTTL`) but no getter. */
|
|
282
|
+
getUnicastHopLimit() {
|
|
283
|
+
onCall("udp-socket.get-unicast-hop-limit");
|
|
284
|
+
return this.#hopLimit ?? 64;
|
|
285
|
+
}
|
|
286
|
+
setUnicastHopLimit(value) {
|
|
287
|
+
onCall("udp-socket.set-unicast-hop-limit");
|
|
288
|
+
if (value < 1) {
|
|
289
|
+
// The WIT pins this: "set-unicast-hop-limit(0)" must fail.
|
|
290
|
+
throw componentError({ kind: "invalid-argument" }, "udp-socket.set-unicast-hop-limit: the hop limit must be at least 1");
|
|
291
|
+
}
|
|
292
|
+
this.#hopLimit = value;
|
|
293
|
+
if (this.#conn !== undefined)
|
|
294
|
+
this.#applyCachedOptions();
|
|
295
|
+
}
|
|
296
|
+
getReceiveBufferSize() {
|
|
297
|
+
onCall("udp-socket.get-receive-buffer-size");
|
|
298
|
+
return this.#bufferSize("receive", this.#recvBuffer, this.#conn?.getRecvBufferSize);
|
|
299
|
+
}
|
|
300
|
+
setReceiveBufferSize(value) {
|
|
301
|
+
onCall("udp-socket.set-receive-buffer-size");
|
|
302
|
+
if (value === 0n) {
|
|
303
|
+
throw componentError({ kind: "invalid-argument" }, "udp-socket.set-receive-buffer-size: zero is not a buffer size");
|
|
304
|
+
}
|
|
305
|
+
this.#recvBuffer = value;
|
|
306
|
+
if (this.#conn !== undefined)
|
|
307
|
+
this.#applyCachedOptions();
|
|
308
|
+
}
|
|
309
|
+
getSendBufferSize() {
|
|
310
|
+
onCall("udp-socket.get-send-buffer-size");
|
|
311
|
+
return this.#bufferSize("send", this.#sendBuffer, this.#conn?.getSendBufferSize);
|
|
312
|
+
}
|
|
313
|
+
setSendBufferSize(value) {
|
|
314
|
+
onCall("udp-socket.set-send-buffer-size");
|
|
315
|
+
if (value === 0n) {
|
|
316
|
+
throw componentError({ kind: "invalid-argument" }, "udp-socket.set-send-buffer-size: zero is not a buffer size");
|
|
317
|
+
}
|
|
318
|
+
this.#sendBuffer = value;
|
|
319
|
+
if (this.#conn !== undefined)
|
|
320
|
+
this.#applyCachedOptions();
|
|
321
|
+
}
|
|
322
|
+
/** Live kernel value when bound (SO_RCVBUF doubling and clamping
|
|
323
|
+
* included), the cached request before that, `not-supported` when
|
|
324
|
+
* neither exists (the OS default is unknowable pre-bind here). */
|
|
325
|
+
#bufferSize(which, cached, live) {
|
|
326
|
+
if (this.#conn !== undefined && live !== undefined) {
|
|
327
|
+
try {
|
|
328
|
+
return BigInt(live.call(this.#conn));
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
throw mapPlatformError(e, `udp-socket.get-${which}-buffer-size`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (cached !== undefined)
|
|
335
|
+
return cached;
|
|
336
|
+
throw componentError({ kind: "not-supported" }, `udp-socket.get-${which}-buffer-size: unknowable before bind on this host`);
|
|
337
|
+
}
|
|
338
|
+
/** Cached options -> the live socket (at bind, and on later sets). */
|
|
339
|
+
#applyCachedOptions() {
|
|
340
|
+
const conn = this.#conn;
|
|
341
|
+
if (conn === undefined)
|
|
342
|
+
return;
|
|
343
|
+
try {
|
|
344
|
+
if (this.#hopLimit !== undefined)
|
|
345
|
+
conn.setTtl?.(this.#hopLimit);
|
|
346
|
+
if (this.#recvBuffer !== undefined) {
|
|
347
|
+
conn.setRecvBufferSize?.(Number(this.#recvBuffer));
|
|
348
|
+
}
|
|
349
|
+
if (this.#sendBuffer !== undefined) {
|
|
350
|
+
conn.setSendBufferSize?.(Number(this.#sendBuffer));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
throw mapPlatformError(e, "udp-socket (applying cached options)");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
[(_connect_decorators = [suspending], Symbol.dispose)]() {
|
|
358
|
+
const conn = this.#conn;
|
|
359
|
+
this.#conn = undefined;
|
|
360
|
+
if (conn !== undefined) {
|
|
361
|
+
try {
|
|
362
|
+
conn.close();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Already closed.
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** Re-detect per call: `create`'s answer must not outlive a test's stub. */
|
|
370
|
+
#listen(opts) {
|
|
371
|
+
const listen = listenDatagram();
|
|
372
|
+
if (listen === undefined) {
|
|
373
|
+
throw componentError({ kind: "not-supported" }, "udp-socket: the datagram backend disappeared after create");
|
|
374
|
+
}
|
|
375
|
+
return listen(opts);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
})();
|
|
379
|
+
let TcpSocket = (() => {
|
|
380
|
+
let _instanceExtraInitializers = [];
|
|
381
|
+
let _listen_decorators;
|
|
382
|
+
return class TcpSocket {
|
|
383
|
+
static {
|
|
384
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
385
|
+
__esDecorate(this, null, _listen_decorators, { kind: "method", name: "listen", static: false, private: false, access: { has: obj => "listen" in obj, get: obj => obj.listen }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
386
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
387
|
+
}
|
|
388
|
+
#family = __runInitializers(this, _instanceExtraInitializers);
|
|
389
|
+
#state = "unbound";
|
|
390
|
+
#conn;
|
|
391
|
+
#listener;
|
|
392
|
+
/** The address `bind` recorded; the OS bind happens at `listen` (header). */
|
|
393
|
+
#localRequest;
|
|
394
|
+
#sendCalled = false;
|
|
395
|
+
#receiveCalled = false;
|
|
396
|
+
/** listen()'s accept-queue hint (`set-listen-backlog-size`). */
|
|
397
|
+
#backlog;
|
|
398
|
+
/** SO_KEEPALIVE + TCP_KEEPIDLE cache (node's exact option surface);
|
|
399
|
+
* applied at connect and on set-while-connected. The idle default is
|
|
400
|
+
* Linux's tcp_keepalive_time (7200 s) — DOCUMENTED, not read from
|
|
401
|
+
* the OS (node exposes no getter). */
|
|
402
|
+
#keepAliveEnabled = false;
|
|
403
|
+
#keepAliveIdleNs = 7200000000000n;
|
|
404
|
+
/**
|
|
405
|
+
* Shared-ownership references (WIT: "The OS socket is closed only
|
|
406
|
+
* after the last handle is dropped"): the resource handle plus each
|
|
407
|
+
* live pump and the accept stream. The conn/listener close at zero —
|
|
408
|
+
* so a live send, receive, or accept stream keeps the socket open
|
|
409
|
+
* past the guest dropping the handle.
|
|
410
|
+
*/
|
|
411
|
+
#refs = 1;
|
|
412
|
+
#handleDropped = false;
|
|
413
|
+
constructor(family) {
|
|
414
|
+
this.#family = family;
|
|
415
|
+
}
|
|
416
|
+
/** An accepted connection, already in the `connected` state. */
|
|
417
|
+
static #accepted(family, conn) {
|
|
418
|
+
const socket = new TcpSocket(family);
|
|
419
|
+
socket.#conn = conn;
|
|
420
|
+
socket.#state = "connected";
|
|
421
|
+
return socket;
|
|
422
|
+
}
|
|
423
|
+
static create(addressFamily) {
|
|
424
|
+
onCall("tcp-socket.create");
|
|
425
|
+
if (tcpConnect() === undefined) {
|
|
426
|
+
throw componentError({ kind: "not-supported" }, "tcp-socket.create: this host provides no TCP sockets (no node:net)");
|
|
427
|
+
}
|
|
428
|
+
return new TcpSocket(addressFamily);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Records the local address; the OS bind is DEFERRED to `listen` or
|
|
432
|
+
* `connect` (recorded divergence: node cannot bind a socket it has
|
|
433
|
+
* not yet connected or listened — so `address-in-use` and friends
|
|
434
|
+
* surface at those calls, with their real codes).
|
|
435
|
+
*/
|
|
436
|
+
bind(localAddress) {
|
|
437
|
+
onCall("tcp-socket.bind");
|
|
438
|
+
if (this.#state !== "unbound") {
|
|
439
|
+
throw componentError({ kind: "invalid-state" }, `tcp-socket.bind: not bindable from the '${this.#state}' state`);
|
|
440
|
+
}
|
|
441
|
+
if (!isValidAddressFamily(this.#family, localAddress)) {
|
|
442
|
+
throw componentError({ kind: "invalid-argument" }, `tcp-socket.bind: address family mismatch (an ${this.#family} socket)`);
|
|
443
|
+
}
|
|
444
|
+
if (localAddress.kind === "ipv6" && localAddress.value.scopeId !== 0) {
|
|
445
|
+
throw componentError({ kind: "not-supported" }, "tcp-socket.bind: non-zero scope-id (not expressible through node addresses)");
|
|
446
|
+
}
|
|
447
|
+
this.#localRequest = localAddress;
|
|
448
|
+
this.#state = "bound";
|
|
449
|
+
}
|
|
450
|
+
async connect(remoteAddress) {
|
|
451
|
+
onCall("tcp-socket.connect");
|
|
452
|
+
if (this.#state !== "unbound" && this.#state !== "bound") {
|
|
453
|
+
// Includes `closed` after a failed attempt: "A single socket can
|
|
454
|
+
// not be used to connect more than once."
|
|
455
|
+
throw componentError({ kind: "invalid-state" }, `tcp-socket.connect: not connectable from the '${this.#state}' state`);
|
|
456
|
+
}
|
|
457
|
+
if (!isValidAddressFamily(this.#family, remoteAddress) ||
|
|
458
|
+
isUnspecified(remoteAddress) ||
|
|
459
|
+
remoteAddress.value.port === 0) {
|
|
460
|
+
throw componentError({ kind: "invalid-argument" }, "tcp-socket.connect: the remote address must be a specific unicast " +
|
|
461
|
+
`address and non-zero port in the socket's family (${this.#family})`);
|
|
462
|
+
}
|
|
463
|
+
if (remoteAddress.kind === "ipv6" && remoteAddress.value.scopeId !== 0) {
|
|
464
|
+
throw componentError({ kind: "not-supported" }, "tcp-socket.connect: non-zero scope-id (not expressible through node addresses)");
|
|
465
|
+
}
|
|
466
|
+
const connect = tcpConnect();
|
|
467
|
+
if (connect === undefined) {
|
|
468
|
+
throw componentError({ kind: "not-supported" }, "tcp-socket: the TCP backend disappeared after create");
|
|
469
|
+
}
|
|
470
|
+
// Connect-from-bound: `bind` recorded the local address; the OS
|
|
471
|
+
// bind happens here, as part of the dial (`net.connect`'s
|
|
472
|
+
// localAddress/localPort) — so bind errors (address-in-use,
|
|
473
|
+
// address-not-bindable) surface at connect, the deferred-bind
|
|
474
|
+
// divergence the module header records.
|
|
475
|
+
const local = this.#localRequest;
|
|
476
|
+
this.#state = "connecting";
|
|
477
|
+
let conn;
|
|
478
|
+
try {
|
|
479
|
+
conn = await connect({
|
|
480
|
+
transport: "tcp",
|
|
481
|
+
hostname: ipHostname(remoteAddress),
|
|
482
|
+
port: remoteAddress.value.port,
|
|
483
|
+
...(local === undefined ? {} : {
|
|
484
|
+
localHostname: ipHostname(local),
|
|
485
|
+
localPort: local.value.port,
|
|
486
|
+
}),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
// "After a failed connection attempt, the socket will be in the
|
|
491
|
+
// `closed` state and the only valid action left is to `drop`".
|
|
492
|
+
this.#state = "closed";
|
|
493
|
+
throw mapPlatformError(e, "tcp-socket.connect");
|
|
494
|
+
}
|
|
495
|
+
if (this.#state !== "connecting") {
|
|
496
|
+
// Disposed while the dial was in flight: nothing owns the fresh
|
|
497
|
+
// conn — close it rather than leak it.
|
|
498
|
+
try {
|
|
499
|
+
conn.close();
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// Already closed.
|
|
503
|
+
}
|
|
504
|
+
throw componentError({ kind: "invalid-state" }, "tcp-socket.connect: the socket was dropped during connect");
|
|
505
|
+
}
|
|
506
|
+
this.#conn = conn;
|
|
507
|
+
this.#state = "connected";
|
|
508
|
+
this.#applyKeepAlive(); // options set before connect reach the OS here
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* WIT: `listen: func() -> result<stream<tcp-socket>, error-code>` —
|
|
512
|
+
* transitions to `listening` and returns the perpetual accept stream,
|
|
513
|
+
* whose elements are connected `TcpSocket` resources (lowered as
|
|
514
|
+
* `own<tcp-socket>` — amendment A13 destroys any element the guest
|
|
515
|
+
* never takes, closing that accepted connection). An unbound socket
|
|
516
|
+
* implicitly binds to the family wildcard with an ephemeral port.
|
|
517
|
+
*
|
|
518
|
+
* SUSPENDING (embedder-api A1/A2, the wasi:io `block` kernel): the OS
|
|
519
|
+
* bind is deferred one event-loop turn by `net.Server.listen` (module
|
|
520
|
+
* header), so this async method awaits the settle and the runtime
|
|
521
|
+
* parks the calling guest frame for that one tick. Full listener
|
|
522
|
+
* fidelity follows: `get-local-address` is real immediately after,
|
|
523
|
+
* and a failed bind is a branded err with its real code
|
|
524
|
+
* (address-in-use, …). Guests that link `listen` auto-select jspi
|
|
525
|
+
* mode on JSPI engines; engines without JSPI would raise `NeedsJspi`
|
|
526
|
+
* here — currently moot everywhere polyengine guests run (JSC lacks
|
|
527
|
+
* multi-memory, and browsers have no sockets).
|
|
528
|
+
*
|
|
529
|
+
* The stream only ends on fatal errors — the listener dying — while
|
|
530
|
+
* per-connection accept failures are skipped, per the WIT's
|
|
531
|
+
* implementors note.
|
|
532
|
+
*/
|
|
533
|
+
async listen() {
|
|
534
|
+
onCall("tcp-socket.listen");
|
|
535
|
+
if (this.#state !== "unbound" && this.#state !== "bound") {
|
|
536
|
+
throw componentError({ kind: "invalid-state" }, `tcp-socket.listen: not listenable from the '${this.#state}' state`);
|
|
537
|
+
}
|
|
538
|
+
const listen = tcpListen();
|
|
539
|
+
if (listen === undefined) {
|
|
540
|
+
throw componentError({ kind: "not-supported" }, "tcp-socket.listen: this host provides no TCP listeners (no node:net)");
|
|
541
|
+
}
|
|
542
|
+
const local = this.#localRequest ?? wildcardAddress(this.#family);
|
|
543
|
+
const listener = listen({
|
|
544
|
+
transport: "tcp",
|
|
545
|
+
hostname: ipHostname(local),
|
|
546
|
+
port: local.value.port,
|
|
547
|
+
...(this.#backlog === undefined ? {} : { backlog: this.#backlog }),
|
|
548
|
+
});
|
|
549
|
+
try {
|
|
550
|
+
await listener.settled(); // the one-tick park (doc comment above)
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
listener.close();
|
|
554
|
+
this.#state = "closed";
|
|
555
|
+
throw mapPlatformError(e, "tcp-socket.listen");
|
|
556
|
+
}
|
|
557
|
+
this.#listener = listener;
|
|
558
|
+
this.#state = "listening";
|
|
559
|
+
this.#refs++; // the accept stream keeps the listener alive
|
|
560
|
+
const family = this.#family;
|
|
561
|
+
const release = () => this.#release();
|
|
562
|
+
const source = (async function* () {
|
|
563
|
+
try {
|
|
564
|
+
for (;;) {
|
|
565
|
+
let conn;
|
|
566
|
+
try {
|
|
567
|
+
conn = await listener.accept();
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
const kind = mapPlatformError(e, "tcp-socket.listen (accept)")
|
|
571
|
+
.payload.kind;
|
|
572
|
+
// Per-connection failures are skipped (the WIT implementors
|
|
573
|
+
// note: "log it and then skip over non-fatal errors");
|
|
574
|
+
// anything else means the LISTENER is dead — closed under
|
|
575
|
+
// us, or never came up — and ends the perpetual stream.
|
|
576
|
+
if (TRANSIENT_ACCEPT_FAILURES.has(kind))
|
|
577
|
+
continue;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
yield TcpSocket.#accepted(family, conn);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
release();
|
|
585
|
+
}
|
|
586
|
+
})();
|
|
587
|
+
// The A13 producer-cancellation hook: when the guest drops the
|
|
588
|
+
// stream while the loop above is PARKED in accept(), the runtime's
|
|
589
|
+
// pump invokes this — closing the listener is what unparks the
|
|
590
|
+
// accept (it rejects; classified fatal; the generator retires).
|
|
591
|
+
return Object.assign(source, {
|
|
592
|
+
cancel: () => {
|
|
593
|
+
try {
|
|
594
|
+
listener.close();
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// Already closed.
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* WIT: `send: func(data: stream<u8>) -> future<result<_, error-code>>`
|
|
604
|
+
* — a sync func; the returned promise is the future source (A12).
|
|
605
|
+
* NEVER throws: the function has no error channel of its own, so every
|
|
606
|
+
* failure — including the state-machine ones — resolves the future as
|
|
607
|
+
* an err value. The argument stream is dropped on failure so its
|
|
608
|
+
* guest-side writer settles instead of parking forever.
|
|
609
|
+
*/
|
|
610
|
+
send(data) {
|
|
611
|
+
onCall("tcp-socket.send");
|
|
612
|
+
if (this.#state !== "connected" || this.#sendCalled || this.#conn === undefined) {
|
|
613
|
+
dropSendSource(data);
|
|
614
|
+
return Promise.resolve(RESULT_INVALID_STATE);
|
|
615
|
+
}
|
|
616
|
+
this.#sendCalled = true;
|
|
617
|
+
return this.#sendPump(this.#conn, data);
|
|
618
|
+
}
|
|
619
|
+
async #sendPump(conn, data) {
|
|
620
|
+
this.#refs++;
|
|
621
|
+
try {
|
|
622
|
+
// Guest-side iteration failures (a peer trap while reading the
|
|
623
|
+
// lifted stream) are deliberately NOT caught: they are not socket
|
|
624
|
+
// errors, and the rejection rides the producer-failure channel.
|
|
625
|
+
for await (const chunk of data) {
|
|
626
|
+
const bytes = chunk instanceof Uint8Array ? chunk : Uint8Array.from(chunk);
|
|
627
|
+
let at = 0;
|
|
628
|
+
while (at < bytes.length) {
|
|
629
|
+
let n;
|
|
630
|
+
try {
|
|
631
|
+
n = await conn.write(bytes.subarray(at));
|
|
632
|
+
}
|
|
633
|
+
catch (e) {
|
|
634
|
+
dropSendSource(data);
|
|
635
|
+
return resultErrOf(e, "tcp-socket.send");
|
|
636
|
+
}
|
|
637
|
+
at += n;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// End of the guest's stream ("the caller should close the stream
|
|
641
|
+
// when it has no more data"): shutdown(SHUT_WR) — the FIN. The
|
|
642
|
+
// future resolves ok only once the full contents are transmitted.
|
|
643
|
+
try {
|
|
644
|
+
await conn.closeWrite();
|
|
645
|
+
}
|
|
646
|
+
catch (e) {
|
|
647
|
+
return resultErrOf(e, "tcp-socket.send");
|
|
648
|
+
}
|
|
649
|
+
return RESULT_OK;
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
this.#release();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* WIT: `receive: func() -> tuple<stream<u8>, future<result<_,
|
|
657
|
+
* error-code>>>`. NEVER throws; a not-connected or repeat call returns
|
|
658
|
+
* a closed stream and an already-err future, per the WIT. The stream
|
|
659
|
+
* ends cleanly (never fake data) on BOTH graceful FIN and abnormal
|
|
660
|
+
* close — the future distinguishes them (`ok` vs `err`). Dropping the
|
|
661
|
+
* stream's reader (guest SHUT_RD) stops the pump, discards queued
|
|
662
|
+
* data, and settles the future ok — the canceller is the observer
|
|
663
|
+
* (the same logic as embedder-api A8's cancelRead ruling).
|
|
664
|
+
*/
|
|
665
|
+
receive() {
|
|
666
|
+
onCall("tcp-socket.receive");
|
|
667
|
+
if (this.#state !== "connected" || this.#receiveCalled || this.#conn === undefined) {
|
|
668
|
+
return [[], Promise.resolve(RESULT_INVALID_STATE)];
|
|
669
|
+
}
|
|
670
|
+
this.#receiveCalled = true;
|
|
671
|
+
const conn = this.#conn;
|
|
672
|
+
this.#refs++;
|
|
673
|
+
let settle;
|
|
674
|
+
const done = new Promise((r) => (settle = r));
|
|
675
|
+
const release = () => this.#release();
|
|
676
|
+
const source = (async function* () {
|
|
677
|
+
try {
|
|
678
|
+
for (;;) {
|
|
679
|
+
// The chunk is node's own buffer (no copy); it is borrowed by
|
|
680
|
+
// the rendezvous until the peer takes it (A5), which is safe —
|
|
681
|
+
// each read hands back a distinct buffer.
|
|
682
|
+
let chunk;
|
|
683
|
+
try {
|
|
684
|
+
chunk = await conn.read(TCP_RECEIVE_CHUNK);
|
|
685
|
+
}
|
|
686
|
+
catch (e) {
|
|
687
|
+
settle(resultErrOf(e, "tcp-socket.receive"));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (chunk === null) {
|
|
691
|
+
settle(RESULT_OK); // graceful FIN from the peer
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (chunk.length > 0)
|
|
695
|
+
yield chunk;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
settle(RESULT_OK); // no-op if already settled (resolve is once)
|
|
700
|
+
release();
|
|
701
|
+
}
|
|
702
|
+
})();
|
|
703
|
+
return [source, done];
|
|
704
|
+
}
|
|
705
|
+
getLocalAddress() {
|
|
706
|
+
onCall("tcp-socket.get-local-address");
|
|
707
|
+
if (this.#state === "listening" && this.#listener !== undefined) {
|
|
708
|
+
const addr = this.#listener.addr;
|
|
709
|
+
if (addr !== null)
|
|
710
|
+
return parseNetAddr(addr);
|
|
711
|
+
// Unreachable in practice: `listen` awaited the settle, after
|
|
712
|
+
// which the listener reports its address. Kept as an honest err
|
|
713
|
+
// rather than a non-null assertion.
|
|
714
|
+
throw componentError({ kind: "invalid-state" }, "tcp-socket.get-local-address: the listener reported no address");
|
|
715
|
+
}
|
|
716
|
+
if (this.#conn === undefined) {
|
|
717
|
+
throw componentError({ kind: "invalid-state" }, "tcp-socket.get-local-address: the socket is not bound");
|
|
718
|
+
}
|
|
719
|
+
return parseNetAddr(this.#conn.localAddr);
|
|
720
|
+
}
|
|
721
|
+
getRemoteAddress() {
|
|
722
|
+
onCall("tcp-socket.get-remote-address");
|
|
723
|
+
if (this.#state !== "connected" || this.#conn === undefined) {
|
|
724
|
+
throw componentError({ kind: "invalid-state" }, "tcp-socket.get-remote-address: the socket is not connected");
|
|
725
|
+
}
|
|
726
|
+
return parseNetAddr(this.#conn.remoteAddr);
|
|
727
|
+
}
|
|
728
|
+
getAddressFamily() {
|
|
729
|
+
onCall("tcp-socket.get-address-family");
|
|
730
|
+
return this.#family;
|
|
731
|
+
}
|
|
732
|
+
getIsListening() {
|
|
733
|
+
onCall("tcp-socket.get-is-listening");
|
|
734
|
+
return this.#state === "listening";
|
|
735
|
+
}
|
|
736
|
+
/** Stored pre-listen and applied as node's `listen` backlog hint;
|
|
737
|
+
* node cannot re-listen, so changing it on a LISTENING socket is
|
|
738
|
+
* `not-supported` (wasmtime re-listens; recorded divergence). */
|
|
739
|
+
setListenBacklogSize(value) {
|
|
740
|
+
onCall("tcp-socket.set-listen-backlog-size");
|
|
741
|
+
if (value === 0n) {
|
|
742
|
+
throw componentError({ kind: "invalid-argument" }, "tcp-socket.set-listen-backlog-size: zero is not a backlog");
|
|
743
|
+
}
|
|
744
|
+
if (this.#state === "listening") {
|
|
745
|
+
throw componentError({ kind: "not-supported" }, "tcp-socket.set-listen-backlog-size: node cannot re-listen an active listener");
|
|
746
|
+
}
|
|
747
|
+
if (this.#state !== "unbound" && this.#state !== "bound") {
|
|
748
|
+
throw componentError({ kind: "invalid-state" }, `tcp-socket.set-listen-backlog-size: not settable in the '${this.#state}' state`);
|
|
749
|
+
}
|
|
750
|
+
// Clamp to a safe int; the OS clamps to SOMAXCONN anyway.
|
|
751
|
+
this.#backlog = Number(value > 0x7fffffffn ? 0x7fffffffn : value);
|
|
752
|
+
}
|
|
753
|
+
getKeepAliveEnabled() {
|
|
754
|
+
onCall("tcp-socket.get-keep-alive-enabled");
|
|
755
|
+
return this.#keepAliveEnabled;
|
|
756
|
+
}
|
|
757
|
+
setKeepAliveEnabled(value) {
|
|
758
|
+
onCall("tcp-socket.set-keep-alive-enabled");
|
|
759
|
+
this.#keepAliveEnabled = value;
|
|
760
|
+
this.#applyKeepAlive();
|
|
761
|
+
}
|
|
762
|
+
/** Stored-value getter (field doc: the default is documented, not
|
|
763
|
+
* read — node has no getter). */
|
|
764
|
+
getKeepAliveIdleTime() {
|
|
765
|
+
onCall("tcp-socket.get-keep-alive-idle-time");
|
|
766
|
+
return this.#keepAliveIdleNs;
|
|
767
|
+
}
|
|
768
|
+
setKeepAliveIdleTime(value) {
|
|
769
|
+
onCall("tcp-socket.set-keep-alive-idle-time");
|
|
770
|
+
if (value < 1n) {
|
|
771
|
+
throw componentError({ kind: "invalid-argument" }, "tcp-socket.set-keep-alive-idle-time: the idle time must be at least 1 ns");
|
|
772
|
+
}
|
|
773
|
+
this.#keepAliveIdleNs = value;
|
|
774
|
+
this.#applyKeepAlive();
|
|
775
|
+
}
|
|
776
|
+
// TCP_KEEPINTVL / TCP_KEEPCNT / IP_TTL / SO_RCVBUF / SO_SNDBUF have no
|
|
777
|
+
// node:net surface at all — answered honestly, not emulated.
|
|
778
|
+
getKeepAliveInterval() {
|
|
779
|
+
onCall("tcp-socket.get-keep-alive-interval");
|
|
780
|
+
throw this.#noOption("keep-alive-interval (TCP_KEEPINTVL)");
|
|
781
|
+
}
|
|
782
|
+
setKeepAliveInterval(_value) {
|
|
783
|
+
onCall("tcp-socket.set-keep-alive-interval");
|
|
784
|
+
throw this.#noOption("keep-alive-interval (TCP_KEEPINTVL)");
|
|
785
|
+
}
|
|
786
|
+
getKeepAliveCount() {
|
|
787
|
+
onCall("tcp-socket.get-keep-alive-count");
|
|
788
|
+
throw this.#noOption("keep-alive-count (TCP_KEEPCNT)");
|
|
789
|
+
}
|
|
790
|
+
setKeepAliveCount(_value) {
|
|
791
|
+
onCall("tcp-socket.set-keep-alive-count");
|
|
792
|
+
throw this.#noOption("keep-alive-count (TCP_KEEPCNT)");
|
|
793
|
+
}
|
|
794
|
+
getHopLimit() {
|
|
795
|
+
onCall("tcp-socket.get-hop-limit");
|
|
796
|
+
throw this.#noOption("hop-limit (IP_TTL)");
|
|
797
|
+
}
|
|
798
|
+
setHopLimit(_value) {
|
|
799
|
+
onCall("tcp-socket.set-hop-limit");
|
|
800
|
+
throw this.#noOption("hop-limit (IP_TTL)");
|
|
801
|
+
}
|
|
802
|
+
getReceiveBufferSize() {
|
|
803
|
+
onCall("tcp-socket.get-receive-buffer-size");
|
|
804
|
+
throw this.#noOption("receive-buffer-size (SO_RCVBUF)");
|
|
805
|
+
}
|
|
806
|
+
setReceiveBufferSize(_value) {
|
|
807
|
+
onCall("tcp-socket.set-receive-buffer-size");
|
|
808
|
+
throw this.#noOption("receive-buffer-size (SO_RCVBUF)");
|
|
809
|
+
}
|
|
810
|
+
getSendBufferSize() {
|
|
811
|
+
onCall("tcp-socket.get-send-buffer-size");
|
|
812
|
+
throw this.#noOption("send-buffer-size (SO_SNDBUF)");
|
|
813
|
+
}
|
|
814
|
+
setSendBufferSize(_value) {
|
|
815
|
+
onCall("tcp-socket.set-send-buffer-size");
|
|
816
|
+
throw this.#noOption("send-buffer-size (SO_SNDBUF)");
|
|
817
|
+
}
|
|
818
|
+
#noOption(what) {
|
|
819
|
+
return componentError({ kind: "not-supported" }, `tcp-socket: node:net exposes no ${what}`);
|
|
820
|
+
}
|
|
821
|
+
/** The keep-alive cache -> the live socket, when there is one. */
|
|
822
|
+
#applyKeepAlive() {
|
|
823
|
+
const conn = this.#conn;
|
|
824
|
+
if (this.#state !== "connected" || conn === undefined)
|
|
825
|
+
return;
|
|
826
|
+
if (conn.setKeepAlive === undefined) {
|
|
827
|
+
throw componentError({ kind: "not-supported" }, "tcp-socket: this host's TCP backend has no keep-alive control");
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
conn.setKeepAlive(this.#keepAliveEnabled, Number(this.#keepAliveIdleNs / 1000000n));
|
|
831
|
+
}
|
|
832
|
+
catch (e) {
|
|
833
|
+
throw mapPlatformError(e, "tcp-socket (applying keep-alive)");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
[(_listen_decorators = [suspending], Symbol.dispose)]() {
|
|
837
|
+
if (this.#handleDropped)
|
|
838
|
+
return;
|
|
839
|
+
this.#handleDropped = true;
|
|
840
|
+
if (this.#state === "unbound" || this.#state === "bound" ||
|
|
841
|
+
this.#state === "connecting") {
|
|
842
|
+
// An in-flight dial observes this and closes its fresh conn.
|
|
843
|
+
this.#state = "closed";
|
|
844
|
+
}
|
|
845
|
+
this.#release();
|
|
846
|
+
}
|
|
847
|
+
#release() {
|
|
848
|
+
this.#refs--;
|
|
849
|
+
if (this.#refs === 0) {
|
|
850
|
+
const conn = this.#conn;
|
|
851
|
+
this.#conn = undefined;
|
|
852
|
+
if (conn !== undefined) {
|
|
853
|
+
try {
|
|
854
|
+
conn.close();
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
// Already closed.
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const listener = this.#listener;
|
|
861
|
+
this.#listener = undefined;
|
|
862
|
+
if (listener !== undefined) {
|
|
863
|
+
try {
|
|
864
|
+
listener.close();
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
// Already closed (e.g. by the accept stream's cancel hook).
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
})();
|
|
874
|
+
/**
|
|
875
|
+
* `wasi:sockets/ip-name-lookup@0.3`: `resolve-addresses: async
|
|
876
|
+
* func(name) -> result<list<ip-address>, error-code>` — getaddrinfo
|
|
877
|
+
* over the platform seam (node:dns `lookup`, i.e. the system resolver,
|
|
878
|
+
* not raw DNS). IP literals resolve locally without touching the
|
|
879
|
+
* resolver (wasmtime parity); answers keep the resolver's order.
|
|
880
|
+
*/
|
|
881
|
+
const resolveAddresses = async (name) => {
|
|
882
|
+
onCall("ip-name-lookup.resolve-addresses");
|
|
883
|
+
const nameErr = (payload, detail) => new ComponentException(payload, `wasi:sockets/ip-name-lookup@0.3: ${detail}`);
|
|
884
|
+
const toIpAddress = (hostname) => {
|
|
885
|
+
const parsed = parseNetAddr({ hostname, port: 0 });
|
|
886
|
+
return parsed.kind === "ipv4"
|
|
887
|
+
? { kind: "ipv4", value: parsed.value.address }
|
|
888
|
+
: { kind: "ipv6", value: parsed.value.address };
|
|
889
|
+
};
|
|
890
|
+
if (name.length === 0) {
|
|
891
|
+
throw nameErr({ kind: "invalid-argument" }, "resolve-addresses: empty name");
|
|
892
|
+
}
|
|
893
|
+
// An IP literal is already an answer (and `lookup` would hand it back
|
|
894
|
+
// unchanged anyway — skip the resolver round-trip).
|
|
895
|
+
try {
|
|
896
|
+
return [toIpAddress(name.startsWith("[") ? name.slice(1, -1) : name)];
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
// Not a literal: a real name for the resolver.
|
|
900
|
+
}
|
|
901
|
+
const lookup = dnsLookup();
|
|
902
|
+
if (lookup === undefined) {
|
|
903
|
+
throw nameErr({ kind: "permanent-resolver-failure" }, "resolve-addresses: this host provides no resolver (no node:dns)");
|
|
904
|
+
}
|
|
905
|
+
let answers;
|
|
906
|
+
try {
|
|
907
|
+
answers = await lookup(name);
|
|
908
|
+
}
|
|
909
|
+
catch (e) {
|
|
910
|
+
const code = e?.code;
|
|
911
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
912
|
+
if (code === "ENOTFOUND" || code === "EAI_NONAME" || code === "ENODATA") {
|
|
913
|
+
throw nameErr({ kind: "name-unresolvable" }, `resolve-addresses: ${message}`);
|
|
914
|
+
}
|
|
915
|
+
if (code === "EAI_AGAIN" || code === "ETIMEOUT" || code === "ETIMEDOUT") {
|
|
916
|
+
throw nameErr({ kind: "temporary-resolver-failure" }, `resolve-addresses: ${message}`);
|
|
917
|
+
}
|
|
918
|
+
if (isDenoError(e, "NotCapable") || isDenoError(e, "PermissionDenied") ||
|
|
919
|
+
code === "EACCES" || code === "EPERM") {
|
|
920
|
+
throw nameErr({ kind: "access-denied" }, `resolve-addresses: ${message}`);
|
|
921
|
+
}
|
|
922
|
+
if (e instanceof TypeError) {
|
|
923
|
+
throw nameErr({ kind: "invalid-argument" }, `resolve-addresses: ${message}`);
|
|
924
|
+
}
|
|
925
|
+
throw nameErr({ kind: "other", value: message }, `resolve-addresses: ${message}`);
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
return answers.map((a) => toIpAddress(a.address));
|
|
929
|
+
}
|
|
930
|
+
catch (e) {
|
|
931
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
932
|
+
throw nameErr({ kind: "other", value: message }, `resolve-addresses: ${message}`);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
return {
|
|
936
|
+
imports: {
|
|
937
|
+
"wasi:sockets/types@0.3": { UdpSocket, TcpSocket },
|
|
938
|
+
"wasi:sockets/ip-name-lookup@0.3": { resolveAddresses },
|
|
939
|
+
},
|
|
940
|
+
UdpSocket,
|
|
941
|
+
TcpSocket,
|
|
942
|
+
resolveAddresses,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/** How many bytes one tcp receive read asks the OS for. */
|
|
946
|
+
const TCP_RECEIVE_CHUNK = 16384;
|
|
947
|
+
/**
|
|
948
|
+
* Accept failures that are per-connection, not per-listener: the WIT
|
|
949
|
+
* implementors note says to skip them ("Guest code never gets to see
|
|
950
|
+
* these failures"); everything else ends the perpetual stream.
|
|
951
|
+
*/
|
|
952
|
+
const TRANSIENT_ACCEPT_FAILURES = new Set([
|
|
953
|
+
"connection-aborted",
|
|
954
|
+
"connection-reset",
|
|
955
|
+
"connection-refused",
|
|
956
|
+
"connection-broken",
|
|
957
|
+
"remote-unreachable",
|
|
958
|
+
"timeout",
|
|
959
|
+
]);
|
|
960
|
+
/**
|
|
961
|
+
* Abandon tcp send's input when the operation fails: a lifted `Stream`
|
|
962
|
+
* handle is dropped so the guest's writer settles ("reader went away")
|
|
963
|
+
* instead of parking forever; other producer shapes are cleaned up by the
|
|
964
|
+
* iteration protocol itself (`for await`'s abrupt-exit `return()`).
|
|
965
|
+
*/
|
|
966
|
+
function dropSendSource(data) {
|
|
967
|
+
if (data instanceof Stream)
|
|
968
|
+
data.drop();
|
|
969
|
+
}
|