@bytecodealliance/preview2-shim 0.14.2 → 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.
@@ -1,219 +1,576 @@
1
- /*
2
- * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily
3
- */
4
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";
5
25
 
6
- const symbolSocketUdpIpUnspecified =
7
- Symbol.symbolSocketUdpIpUnspecified ??
8
- Symbol.for("symbolSocketUdpIpUnspecified");
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;
9
31
 
10
- /** @type {Map<number, NodeJS.Socket>} */
11
- export const openedSockets = new Map();
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
+ */
12
62
 
13
- /** @type {Map<number, Map<string, { data: Buffer, rinfo: { address: string, family: string, port: number, size: number } }>>} */
14
- const queuedReceivedSocketDatagrams = new Map();
63
+ let udpSocketCnt = 0,
64
+ datagramStreamCnt = 0;
15
65
 
16
- let socketCnt = 0;
66
+ /**
67
+ * @type {Map<number, UdpSocketRecord>}
68
+ */
69
+ export const udpSockets = new Map();
17
70
 
18
- export function getSocketOrThrow(socketId) {
19
- const socket = openedSockets.get(socketId);
20
- if (!socket) throw "invalid-state";
21
- return socket;
22
- }
71
+ /**
72
+ * @type {Map<number, DatagramStreamRecord>}
73
+ */
74
+ export const datagramStreams = new Map();
23
75
 
24
- export function getSocketByPort(port) {
25
- return Array.from(openedSockets.values()).find(
26
- (socket) => socket.address().port === port
27
- );
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;
28
102
  }
29
103
 
30
- export function getBoundSockets(socketId) {
31
- return Array.from(openedSockets.entries())
32
- .filter(([id, _socket]) => id !== socketId) // exclude source socket
33
- .map(([_id, socket]) => socket.address());
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;
34
146
  }
35
147
 
36
- export function dequeueReceivedSocketDatagram(socketInfo, maxResults) {
37
- const key = `PORT:${socketInfo.port}`;
38
- const dgrams = queuedReceivedSocketDatagrams
39
- .get(key)
40
- .splice(0, Number(maxResults));
41
- return dgrams;
42
- }
43
- export function enqueueReceivedSocketDatagram(socketInfo, { data, rinfo }) {
44
- const key = `PORT:${socketInfo.port}`;
45
- const chunk = {
46
- data,
47
- rinfo, // sender/remote socket info (source)
48
- socketInfo, // receiver socket info (targeted socket)
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 },
49
161
  };
50
-
51
- // create new queue if not exists
52
- if (!queuedReceivedSocketDatagrams.has(key)) {
53
- queuedReceivedSocketDatagrams.set(key, []);
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);
54
171
  }
172
+ return datagramStream;
173
+ }
174
+
175
+ export function socketUdpBindStart(id, localAddress, family) {
176
+ const socket = udpSockets.get(id);
55
177
 
56
- // append to queue
57
- const queue = queuedReceivedSocketDatagrams.get(key);
58
- queue.push(chunk);
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
+ );
59
202
  }
60
203
 
61
- //-----------------------------------------------------
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
+ }
62
226
 
63
227
  /**
64
- * @param {IpAddressFamily} addressFamily
65
- * @returns {NodeJS.Socket}
228
+ * @param {number} id
229
+ * @returns {IpSocketAddress}
66
230
  */
67
- export function createUdpSocket(addressFamily, reuseAddr) {
68
- return new Promise((resolve, reject) => {
69
- const type = addressFamily === "ipv6" ? "udp6" : "udp4";
70
- try {
71
- const socket = createSocket({
72
- type,
73
- reuseAddr,
74
- });
75
- openedSockets.set(++socketCnt, socket);
76
- resolve({
77
- id: socketCnt,
78
- socket,
79
- });
80
- } catch (e) {
81
- reject(e);
82
- }
83
- });
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);
84
240
  }
85
241
 
86
- export function socketUdpBind(id, payload) {
87
- const { localAddress, localPort } = payload;
88
- const socket = getSocketOrThrow(id);
89
-
90
- // Note: even if the client has bound to IPV4_UNSPECIFIED/IPV6_UNSPECIFIED (0.0.0.0 // ::),
91
- // rinfo.address is resolved to IPV4_LOOPBACK/IPV6_LOOPBACK.
92
- // We need to cache the original bound IP type and fix rinfo.address when receiving datagrams (see below)
93
- // See https://github.com/WebAssembly/wasi-sockets/issues/86
94
- socket[symbolSocketUdpIpUnspecified] = {
95
- isUnspecified:
96
- localAddress === "0.0.0.0" || localAddress === "0:0:0:0:0:0:0:0",
97
- localAddress,
98
- };
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
+ }
99
256
 
100
- return new Promise((resolve) => {
101
- socket.bind(
102
- {
103
- address: localAddress,
104
- port: localPort,
105
- },
106
- () => {
107
- openedSockets.set(id, socket);
108
- resolve(0);
109
- }
110
- );
257
+ export function socketUdpStream(id, remoteAddress) {
258
+ const socket = udpSockets.get(id);
259
+ const { udpSocket } = socket;
111
260
 
112
- socket.on("message", (data, rinfo) => {
113
- const remoteSocket = getSocketByPort(rinfo.port);
114
- let { address, port } = socket.address();
261
+ if (
262
+ socket.state !== SOCKET_STATE_BOUND &&
263
+ socket.state !== SOCKET_STATE_CONNECTION
264
+ )
265
+ throw "invalid-state";
115
266
 
116
- if (remoteSocket[symbolSocketUdpIpUnspecified].isUnspecified) {
117
- // cache original bound address
118
- rinfo._address =
119
- remoteSocket[symbolSocketUdpIpUnspecified].localAddress;
120
- }
267
+ if (socket.state === SOCKET_STATE_INIT && !remoteAddress)
268
+ throw "invalid-state";
121
269
 
122
- const receiverSocket = {
123
- address,
124
- port,
125
- id,
126
- };
270
+ if (
271
+ remoteAddress &&
272
+ (remoteAddress.val.port === 0 ||
273
+ isWildcardAddress(remoteAddress) ||
274
+ (remoteAddress.tag === "ipv6" && isIPv4MappedAddress(remoteAddress)))
275
+ )
276
+ throw "invalid-argument";
127
277
 
128
- enqueueReceivedSocketDatagram(receiverSocket, { data, rinfo });
129
- });
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
+ }
130
287
 
131
- // catch all errors
132
- socket.once("error", (err) => {
133
- resolve(err.errno);
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);
134
315
  });
135
- });
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
+ }
136
325
  }
137
326
 
138
- export function socketUdpCheckSend(id) {
139
- const socket = getSocketOrThrow(id);
140
- try {
141
- return socket.getSendBufferSize() - socket.getSendQueueSize();
142
- } catch (err) {
143
- return err.errno;
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
+ }
144
339
  }
340
+ socket.receiveBufferSize = bufferSize;
145
341
  }
146
342
 
147
- export function socketUdpSend(id, payload) {
148
- let { remoteHost, remotePort, data } = payload;
149
- const socket = getSocketOrThrow(id);
150
-
151
- return new Promise((resolve) => {
152
- const _callback = (err, _byteLength) => {
153
- if (err) return resolve(err.errno);
154
- resolve(0); // success
155
- };
156
-
157
- // Note: when remoteHost/remotePort is None, we broadcast to all bound sockets
158
- // except the source socket
159
- if (remotePort === undefined || remoteHost === undefined) {
160
- getBoundSockets(id).forEach((adr) => {
161
- socket.send(data, adr.port, adr.address, _callback);
162
- });
163
- } else {
164
- socket.send(data, remotePort, remoteHost, _callback);
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);
165
354
  }
355
+ }
356
+ socket.sendBufferSize = bufferSize;
357
+ }
166
358
 
167
- socket.once("error", (err) => {
168
- resolve(err.errno);
169
- });
170
- });
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;
171
372
  }
172
373
 
173
- export function SocketUdpReceive(id, payload) {
174
- const { maxResults } = payload;
175
- const socket = getSocketOrThrow(id);
176
- const { address, port } = socket.address();
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
+ }
177
394
 
178
- // set target socket info
179
- // we use this to filter out datagrams that are were sent to this socket
180
- const targetSocket = {
181
- address,
182
- port,
183
- };
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
+ }
184
413
 
185
- const dgrams = dequeueReceivedSocketDatagram(targetSocket, maxResults);
186
- return Promise.resolve(dgrams);
414
+ export function socketUdpGetUnicastHopLimit(id) {
415
+ const { unicastHopLimit } = udpSockets.get(id);
416
+ return unicastHopLimit;
187
417
  }
188
418
 
189
- export function socketUdpConnect(id, payload) {
190
- const socket = getSocketOrThrow(id);
191
- const { remoteAddress, remotePort } = payload;
419
+ export function socketUdpDispose(id) {
420
+ const { udpSocket } = udpSockets.get(id);
192
421
  return new Promise((resolve) => {
193
- socket.connect(remotePort, remoteAddress, () => {
194
- openedSockets.set(id, socket);
422
+ udpSocket.close(() => {
423
+ udpSockets.delete(id);
195
424
  resolve(0);
196
425
  });
197
- socket.once("error", (err) => {
198
- resolve(err.errno);
199
- });
200
426
  });
201
427
  }
202
428
 
203
- export function socketUdpDisconnect(id) {
204
- const socket = getSocketOrThrow(id);
205
- return new Promise((resolve) => {
206
- socket.disconnect();
207
- resolve(0);
208
- });
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));
209
438
  }
210
439
 
211
- export function socketUdpDispose(id) {
212
- const socket = getSocketOrThrow(id);
213
- return new Promise((resolve) => {
214
- socket.close(() => {
215
- openedSockets.delete(id);
216
- resolve(0);
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
+ }
217
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
+ }
218
545
  });
219
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
+ }