@bytecodealliance/preview2-shim 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,576 @@
1
+ import { createSocket } from "node:dgram";
2
+ import {
3
+ createFuture,
4
+ futureDispose,
5
+ futureTakeValue,
6
+ pollStateReady,
7
+ verifyPollsDroppedForDrop,
8
+ } from "./worker-thread.js";
9
+ import {
10
+ convertSocketError,
11
+ convertSocketErrorCode,
12
+ getDefaultReceiveBufferSize,
13
+ getDefaultSendBufferSize,
14
+ ipSocketAddress,
15
+ isIPv4MappedAddress,
16
+ isWildcardAddress,
17
+ noLookup,
18
+ serializeIpAddress,
19
+ SOCKET_STATE_BIND,
20
+ SOCKET_STATE_BOUND,
21
+ SOCKET_STATE_CLOSED,
22
+ SOCKET_STATE_CONNECTION,
23
+ SOCKET_STATE_INIT,
24
+ } from "./worker-sockets.js";
25
+
26
+ // Experimental support for batched UDP sends. Set this to true to enable.
27
+ // This is not enabled by default because we need to figure out how to know
28
+ // how many datagrams were sent when there is an error in a batch.
29
+ // See the err path in "handler" in the "doSendBatch" of socketOutgoingDatagramStreamSend.
30
+ const UDP_BATCH_SENDS = false;
31
+
32
+ /**
33
+ * @typedef {import("../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress
34
+ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily
35
+ *
36
+ *
37
+ * @typedef {{
38
+ * state: number,
39
+ * remoteAddress: string | null,
40
+ * remotePort: number | null,
41
+ * sendBufferSize: number | null,
42
+ * receiveBufferSize: number | null,
43
+ * unicastHopLimit: number,
44
+ * udpSocket: import('node:dgram').Socket,
45
+ * future: number | null,
46
+ * serializedLocalAddress: string | null,
47
+ * pollState: PollState,
48
+ * incomingDatagramStream: number | null,
49
+ * outgoingDatagramStream: number | null,
50
+ * }} UdpSocketRecord
51
+ *
52
+ * @typedef {{
53
+ * active: bool,
54
+ * error: any | null,
55
+ * socket: UdpSocketRecord,
56
+ * pollState: PollState,
57
+ * queue?: Buffer[],
58
+ * cleanup: () => void | null,
59
+ * }} DatagramStreamRecord
60
+ *
61
+ */
62
+
63
+ let udpSocketCnt = 0,
64
+ datagramStreamCnt = 0;
65
+
66
+ /**
67
+ * @type {Map<number, UdpSocketRecord>}
68
+ */
69
+ export const udpSockets = new Map();
70
+
71
+ /**
72
+ * @type {Map<number, DatagramStreamRecord>}
73
+ */
74
+ export const datagramStreams = new Map();
75
+
76
+ /**
77
+ * @param {IpAddressFamily} addressFamily
78
+ * @returns {number}
79
+ */
80
+ export function createUdpSocket({ family, unicastHopLimit }) {
81
+ const udpSocket = createSocket({
82
+ type: family === "ipv6" ? "udp6" : "udp4",
83
+ reuseAddr: false,
84
+ ipv6Only: family === "ipv6",
85
+ lookup: noLookup,
86
+ });
87
+ udpSockets.set(++udpSocketCnt, {
88
+ state: SOCKET_STATE_INIT,
89
+ remoteAddress: null,
90
+ remotePort: null,
91
+ sendBufferSize: null,
92
+ receiveBufferSize: null,
93
+ unicastHopLimit,
94
+ udpSocket,
95
+ future: null,
96
+ serializedLocalAddress: null,
97
+ pollState: { ready: true, listener: null, polls: [], parentStream: null },
98
+ incomingDatagramStream: null,
99
+ outgoingDatagramStream: null,
100
+ });
101
+ return udpSocketCnt;
102
+ }
103
+
104
+ /**
105
+ * @param {UdpSocketRecord} socket
106
+ * @returns {DatagramStreamRecord}
107
+ */
108
+ function createIncomingDatagramStream(socket) {
109
+ const id = ++datagramStreamCnt;
110
+ const pollState = {
111
+ ready: false,
112
+ listener: null,
113
+ polls: [],
114
+ parentStream: null,
115
+ };
116
+ const datagramStream = {
117
+ id,
118
+ active: true,
119
+ error: null,
120
+ socket,
121
+ queue: [],
122
+ cleanup,
123
+ pollState,
124
+ };
125
+ const { udpSocket } = socket;
126
+ datagramStreams.set(id, datagramStream);
127
+ function cleanup() {
128
+ udpSocket.off("message", onMessage);
129
+ udpSocket.off("error", onError);
130
+ }
131
+ function onMessage(data, rinfo) {
132
+ const family = rinfo.family.toLowerCase();
133
+ datagramStream.queue.push({
134
+ data,
135
+ remoteAddress: ipSocketAddress(family, rinfo.address, rinfo.port),
136
+ });
137
+ if (!pollState.ready) pollStateReady(pollState);
138
+ }
139
+ function onError(err) {
140
+ datagramStream.error = err;
141
+ pollStateReady(datagramStream.pollState);
142
+ }
143
+ udpSocket.on("message", onMessage);
144
+ udpSocket.once("error", onError);
145
+ return datagramStream;
146
+ }
147
+
148
+ /**
149
+ * @param {UdpSocketRecord} socket
150
+ * @returns {DatagramStreamRecord}
151
+ */
152
+ function createOutgoingDatagramStream(socket) {
153
+ const id = ++datagramStreamCnt;
154
+ const datagramStream = {
155
+ id,
156
+ active: true,
157
+ error: null,
158
+ socket,
159
+ cleanup,
160
+ pollState: { ready: true, listener: null, polls: [], parentStream: null },
161
+ };
162
+ const { udpSocket } = socket;
163
+ datagramStreams.set(id, datagramStream);
164
+ udpSocket.on("error", onError);
165
+ function onError(err) {
166
+ datagramStream.error = err;
167
+ pollStateReady(datagramStream.pollState);
168
+ }
169
+ function cleanup() {
170
+ udpSocket.off("error", onError);
171
+ }
172
+ return datagramStream;
173
+ }
174
+
175
+ export function socketUdpBindStart(id, localAddress, family) {
176
+ const socket = udpSockets.get(id);
177
+
178
+ if (family !== localAddress.tag || isIPv4MappedAddress(localAddress))
179
+ throw "invalid-argument";
180
+
181
+ const serializedLocalAddress = serializeIpAddress(localAddress);
182
+
183
+ if (socket.state !== SOCKET_STATE_INIT) throw "invalid-state";
184
+ socket.state = SOCKET_STATE_BIND;
185
+ const { udpSocket } = socket;
186
+ socket.future = createFuture(
187
+ new Promise((resolve, reject) => {
188
+ function bindOk() {
189
+ resolve();
190
+ udpSocket.off("error", bindErr);
191
+ }
192
+ function bindErr(err) {
193
+ reject(convertSocketError(err));
194
+ udpSocket.off("listening", bindOk);
195
+ }
196
+ udpSocket.once("listening", bindOk);
197
+ udpSocket.once("error", bindErr);
198
+ udpSocket.bind(localAddress.val.port, serializedLocalAddress);
199
+ }),
200
+ socket.pollState
201
+ );
202
+ }
203
+
204
+ export function socketUdpBindFinish(id) {
205
+ const socket = udpSockets.get(id);
206
+ if (socket.state !== SOCKET_STATE_BIND) throw "not-in-progress";
207
+ if (!socket.pollState.ready) throw "would-block";
208
+ const { tag, val } = futureTakeValue(socket.future).val;
209
+ futureDispose(socket.future, false);
210
+ socket.future = null;
211
+ if (tag === "err") {
212
+ socket.state = SOCKET_STATE_CLOSED;
213
+ throw val;
214
+ } else {
215
+ // once bound, we can now set the options
216
+ // since Node.js doesn't support setting them until bound
217
+ socket.udpSocket.setTTL(socket.unicastHopLimit);
218
+ if (socket.sendBufferSize)
219
+ socket.udpSocket.setRecvBufferSize(socket.sendBufferSize);
220
+ if (socket.receieveBufferSize)
221
+ socket.udpSocket.setSendBufferSize(socket.receiveBufferSize);
222
+ socket.state = SOCKET_STATE_BOUND;
223
+ return val;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * @param {number} id
229
+ * @returns {IpSocketAddress}
230
+ */
231
+ export function socketUdpGetLocalAddress(id) {
232
+ const { udpSocket } = udpSockets.get(id);
233
+ let address, family, port;
234
+ try {
235
+ ({ address, family, port } = udpSocket.address());
236
+ } catch (err) {
237
+ throw convertSocketError(err);
238
+ }
239
+ return ipSocketAddress(family.toLowerCase(), address, port);
240
+ }
241
+
242
+ /**
243
+ * @param {number} id
244
+ * @returns {IpSocketAddress}
245
+ */
246
+ export function socketUdpGetRemoteAddress(id) {
247
+ const { udpSocket } = udpSockets.get(id);
248
+ let address, family, port;
249
+ try {
250
+ ({ address, family, port } = udpSocket.remoteAddress());
251
+ } catch (err) {
252
+ throw convertSocketError(err);
253
+ }
254
+ return ipSocketAddress(family.toLowerCase(), address, port);
255
+ }
256
+
257
+ export function socketUdpStream(id, remoteAddress) {
258
+ const socket = udpSockets.get(id);
259
+ const { udpSocket } = socket;
260
+
261
+ if (
262
+ socket.state !== SOCKET_STATE_BOUND &&
263
+ socket.state !== SOCKET_STATE_CONNECTION
264
+ )
265
+ throw "invalid-state";
266
+
267
+ if (socket.state === SOCKET_STATE_INIT && !remoteAddress)
268
+ throw "invalid-state";
269
+
270
+ if (
271
+ remoteAddress &&
272
+ (remoteAddress.val.port === 0 ||
273
+ isWildcardAddress(remoteAddress) ||
274
+ (remoteAddress.tag === "ipv6" && isIPv4MappedAddress(remoteAddress)))
275
+ )
276
+ throw "invalid-argument";
277
+
278
+ if (socket.state === SOCKET_STATE_CONNECTION) {
279
+ socketDatagramStreamClear(socket.incomingDatagramStream);
280
+ socketDatagramStreamClear(socket.outgoingDatagramStream);
281
+ try {
282
+ udpSocket.disconnect();
283
+ } catch (e) {
284
+ throw convertSocketErrorCode(e);
285
+ }
286
+ }
287
+
288
+ if (remoteAddress) {
289
+ const serializedRemoteAddress = serializeIpAddress(remoteAddress);
290
+ socket.remoteAddress = serializedRemoteAddress;
291
+ socket.remotePort = remoteAddress.val.port;
292
+ return new Promise((resolve, reject) => {
293
+ function connectOk() {
294
+ if (socket.state === SOCKET_STATE_INIT) {
295
+ socket.udpSocket.setTTL(socket.unicastHopLimit);
296
+ socket.udpSocket.setRecvBufferSize(socket.sendBufferSize);
297
+ socket.udpSocket.setSendBufferSize(socket.receiveBufferSize);
298
+ }
299
+ udpSocket.off("error", connectErr);
300
+ socket.state = SOCKET_STATE_CONNECTION;
301
+ resolve([
302
+ (socket.incomingDatagramStream = createIncomingDatagramStream(socket))
303
+ .id,
304
+ (socket.outgoingDatagramStream = createOutgoingDatagramStream(socket))
305
+ .id,
306
+ ]);
307
+ }
308
+ function connectErr(err) {
309
+ udpSocket.off("connect", connectOk);
310
+ reject(convertSocketError(err));
311
+ }
312
+ udpSocket.once("connect", connectOk);
313
+ udpSocket.once("error", connectErr);
314
+ udpSocket.connect(remoteAddress.val.port, serializedRemoteAddress);
315
+ });
316
+ } else {
317
+ socket.state = SOCKET_STATE_BOUND;
318
+ socket.remoteAddress = null;
319
+ socket.remotePort = null;
320
+ return [
321
+ (socket.incomingDatagramStream = createIncomingDatagramStream(socket)).id,
322
+ (socket.outgoingDatagramStream = createOutgoingDatagramStream(socket)).id,
323
+ ];
324
+ }
325
+ }
326
+
327
+ export function socketUdpSetReceiveBufferSize(id, bufferSize) {
328
+ const socket = udpSockets.get(id);
329
+ bufferSize = Number(bufferSize);
330
+ if (
331
+ socket.state !== SOCKET_STATE_INIT &&
332
+ socket.state !== SOCKET_STATE_BIND
333
+ ) {
334
+ try {
335
+ socket.udpSocket.setRecvBufferSize(bufferSize);
336
+ } catch (err) {
337
+ throw convertSocketError(err);
338
+ }
339
+ }
340
+ socket.receiveBufferSize = bufferSize;
341
+ }
342
+
343
+ export function socketUdpSetSendBufferSize(id, bufferSize) {
344
+ const socket = udpSockets.get(id);
345
+ bufferSize = Number(bufferSize);
346
+ if (
347
+ socket.state !== SOCKET_STATE_INIT &&
348
+ socket.state !== SOCKET_STATE_BIND
349
+ ) {
350
+ try {
351
+ socket.udpSocket.setSendBufferSize(bufferSize);
352
+ } catch (err) {
353
+ throw convertSocketError(err);
354
+ }
355
+ }
356
+ socket.sendBufferSize = bufferSize;
357
+ }
358
+
359
+ export function socketUdpSetUnicastHopLimit(id, hopLimit) {
360
+ const socket = udpSockets.get(id);
361
+ if (
362
+ socket.state !== SOCKET_STATE_INIT &&
363
+ socket.state !== SOCKET_STATE_BIND
364
+ ) {
365
+ try {
366
+ socket.udpSocket.setTTL(hopLimit);
367
+ } catch (err) {
368
+ throw convertSocketError(err);
369
+ }
370
+ }
371
+ socket.unicastHopLimit = hopLimit;
372
+ }
373
+
374
+ export async function socketUdpGetReceiveBufferSize(id) {
375
+ const socket = udpSockets.get(id);
376
+ if (socket.receiveBufferSize) return BigInt(socket.receiveBufferSize);
377
+ if (
378
+ socket.state !== SOCKET_STATE_INIT &&
379
+ socket.state !== SOCKET_STATE_BIND
380
+ ) {
381
+ try {
382
+ return BigInt(
383
+ (socket.receiveBufferSize = socket.udpSocket.getRecvBufferSize())
384
+ );
385
+ } catch (err) {
386
+ throw convertSocketError(err);
387
+ }
388
+ } else {
389
+ return BigInt(
390
+ (socket.receiveBufferSize = await getDefaultReceiveBufferSize())
391
+ );
392
+ }
393
+ }
394
+
395
+ export async function socketUdpGetSendBufferSize(id) {
396
+ const socket = udpSockets.get(id);
397
+ if (socket.sendBufferSize) return BigInt(socket.sendBufferSize);
398
+ if (
399
+ socket.state !== SOCKET_STATE_INIT &&
400
+ socket.state !== SOCKET_STATE_BIND
401
+ ) {
402
+ try {
403
+ return BigInt(
404
+ (socket.sendBufferSize = socket.udpSocket.getSendBufferSize())
405
+ );
406
+ } catch (err) {
407
+ throw convertSocketError(err);
408
+ }
409
+ } else {
410
+ return BigInt((socket.sendBufferSize = await getDefaultSendBufferSize()));
411
+ }
412
+ }
413
+
414
+ export function socketUdpGetUnicastHopLimit(id) {
415
+ const { unicastHopLimit } = udpSockets.get(id);
416
+ return unicastHopLimit;
417
+ }
418
+
419
+ export function socketUdpDispose(id) {
420
+ const { udpSocket } = udpSockets.get(id);
421
+ return new Promise((resolve) => {
422
+ udpSocket.close(() => {
423
+ udpSockets.delete(id);
424
+ resolve(0);
425
+ });
426
+ });
427
+ }
428
+
429
+ export function socketIncomingDatagramStreamReceive(id, maxResults) {
430
+ const datagramStream = datagramStreams.get(id);
431
+ if (!datagramStream.active)
432
+ throw new Error(
433
+ "wasi-io trap: attempt to receive on inactive incoming datagram stream"
434
+ );
435
+ if (maxResults === 0n || datagramStream.queue.length === 0) return [];
436
+ if (datagramStream.error) throw convertSocketError(datagramStream.error);
437
+ return datagramStream.queue.splice(0, Number(maxResults));
438
+ }
439
+
440
+ export async function socketOutgoingDatagramStreamSend(id, datagrams) {
441
+ const { active, socket } = datagramStreams.get(id);
442
+ if (!active)
443
+ throw new Error(
444
+ "wasi-io trap: writing to inactive outgoing datagram stream"
445
+ );
446
+
447
+ const { udpSocket } = socket;
448
+ let sendQueue = [],
449
+ sendQueueAddress,
450
+ sendQueuePort;
451
+ let datagramsSent = 0;
452
+ for (const { data, remoteAddress } of datagrams) {
453
+ const address = remoteAddress
454
+ ? serializeIpAddress(remoteAddress)
455
+ : socket.remoteAddress;
456
+ const port = remoteAddress?.val.port ?? socket.remotePort;
457
+ let sendLastBatch = !UDP_BATCH_SENDS;
458
+ if (sendQueue.length > 0) {
459
+ if (sendQueueAddress === address && sendQueuePort === port) {
460
+ sendQueue.push(data);
461
+ } else {
462
+ sendLastBatch = true;
463
+ }
464
+ } else {
465
+ sendQueueAddress = address;
466
+ sendQueuePort = port;
467
+ sendQueue.push(data);
468
+ }
469
+ if (sendLastBatch) {
470
+ const err = await doSendBatch();
471
+ if (err) return BigInt(datagramsSent);
472
+ if (UDP_BATCH_SENDS) {
473
+ sendQueue = [data];
474
+ sendQueuePort = port;
475
+ sendQueueAddress = address;
476
+ } else {
477
+ sendQueue = [];
478
+ sendQueuePort = port;
479
+ sendQueueAddress = address;
480
+ }
481
+ }
482
+ }
483
+ if (sendQueue.length) {
484
+ const err = await doSendBatch();
485
+ if (err) return BigInt(datagramsSent);
486
+ }
487
+
488
+ if (datagramsSent !== datagrams.length)
489
+ throw new Error("wasi-io trap: expected to have sent all the datagrams");
490
+ return BigInt(datagramsSent);
491
+
492
+ function doSendBatch() {
493
+ return new Promise((resolve, reject) => {
494
+ if (socket.remoteAddress) {
495
+ if (sendQueueAddress !== socket.remoteAddress || sendQueuePort !== socket.remotePort)
496
+ return void reject("invalid-argument");
497
+ udpSocket.send(sendQueue, handler);
498
+ } else {
499
+ if (!sendQueueAddress)
500
+ return void reject("invalid-argument");
501
+ udpSocket.send(sendQueue, sendQueuePort, sendQueueAddress, handler);
502
+ }
503
+ function handler(err, _sentBytes) {
504
+ if (err) {
505
+ // TODO: update datagramsSent properly on error for multiple sends
506
+ // to enable send batching. Perhaps a Node.js PR could
507
+ // still set the second sendBytes arg?
508
+ if (datagramsSent > 0) resolve(datagramsSent);
509
+ else reject(convertSocketError(err));
510
+ return;
511
+ }
512
+ datagramsSent += sendQueue.length;
513
+ resolve(false);
514
+ }
515
+ });
516
+ }
517
+ }
518
+
519
+ function checkSend(socket) {
520
+ try {
521
+ return Math.floor(
522
+ (socket.udpSocket.getSendBufferSize() -
523
+ socket.udpSocket.getSendQueueSize()) /
524
+ 1500
525
+ );
526
+ } catch (err) {
527
+ throw convertSocketError(err);
528
+ }
529
+ }
530
+
531
+ function pollSend(socket) {
532
+ socket.pollState.ready = false;
533
+ // The only way we have of dealing with getting a backpressure
534
+ // ready signal in Node.js is to just poll on the queue reducing.
535
+ // Ideally this should implement backoff on the poll interval,
536
+ // but that work should be done alongside careful benchmarking
537
+ // in due course.
538
+ setTimeout(() => {
539
+ const remaining = checkSend(socket);
540
+ if (remaining > 0) {
541
+ pollStateReady(socket.pollState);
542
+ } else {
543
+ pollSend(socket);
544
+ }
545
+ });
546
+ }
547
+
548
+ export function socketOutgoingDatagramStreamCheckSend(id) {
549
+ const { active, socket } = datagramStreams.get(id);
550
+ if (!active)
551
+ throw new Error(
552
+ "wasi-io trap: check send on inactive outgoing datagram stream"
553
+ );
554
+ const remaining = checkSend(socket);
555
+ if (remaining <= 0) pollSend(socket);
556
+ return BigInt(remaining);
557
+ }
558
+
559
+ function socketDatagramStreamClear(datagramStream) {
560
+ datagramStream.active = false;
561
+ if (datagramStream.cleanup) {
562
+ datagramStream.cleanup();
563
+ datagramStream.cleanup = null;
564
+ }
565
+ }
566
+
567
+ export function socketDatagramStreamDispose(id) {
568
+ const datagramStream = datagramStreams.get(id);
569
+ datagramStream.active = false;
570
+ if (datagramStream.cleanup) {
571
+ datagramStream.cleanup();
572
+ datagramStream.cleanup = null;
573
+ }
574
+ verifyPollsDroppedForDrop(datagramStream.pollState, "datagram stream");
575
+ datagramStreams.delete(id);
576
+ }