@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.
@@ -1,20 +1,36 @@
1
- import { resolve } from "node:dns/promises";
2
1
  import { createReadStream, createWriteStream } from "node:fs";
3
- import { hrtime } from "node:process";
2
+ import { hrtime, stderr, stdout } from "node:process";
3
+ import { PassThrough } from "node:stream";
4
+ import { format } from "node:util";
4
5
  import { runAsWorker } from "../synckit/index.js";
5
- import { createHttpRequest } from "./worker-http.js";
6
- import { Writable } from "node:stream";
6
+ import {
7
+ clearOutgoingResponse,
8
+ createHttpRequest,
9
+ setOutgoingResponse,
10
+ startHttpServer,
11
+ stopHttpServer,
12
+ } from "./worker-http.js";
13
+ import { Readable } from "node:stream";
14
+ import { read } from "node:fs";
15
+ import { nextTick } from "node:process";
7
16
  import {
8
17
  CALL_MASK,
9
- CALL_SHIFT,
10
18
  CALL_TYPE_MASK,
11
19
  CLOCKS_DURATION_SUBSCRIBE,
12
20
  CLOCKS_INSTANT_SUBSCRIBE,
13
21
  CLOCKS_NOW,
22
+ FILE,
14
23
  FUTURE_DISPOSE,
15
- FUTURE_GET_VALUE_AND_DISPOSE,
24
+ FUTURE_SUBSCRIBE,
25
+ FUTURE_TAKE_VALUE,
26
+ HTTP,
16
27
  HTTP_CREATE_REQUEST,
28
+ HTTP_OUTGOING_BODY_DISPOSE,
17
29
  HTTP_OUTPUT_STREAM_FINISH,
30
+ HTTP_SERVER_CLEAR_OUTGOING_RESPONSE,
31
+ HTTP_SERVER_SET_OUTGOING_RESPONSE,
32
+ HTTP_SERVER_START,
33
+ HTTP_SERVER_STOP,
18
34
  INPUT_STREAM_BLOCKING_READ,
19
35
  INPUT_STREAM_BLOCKING_SKIP,
20
36
  INPUT_STREAM_CREATE,
@@ -32,90 +48,243 @@ import {
32
48
  OUTPUT_STREAM_FLUSH,
33
49
  OUTPUT_STREAM_SPLICE,
34
50
  OUTPUT_STREAM_SUBSCRIBE,
35
- OUTPUT_STREAM_WRITE_ZEROES,
36
51
  OUTPUT_STREAM_WRITE,
37
- POLL_POLL_LIST,
52
+ OUTPUT_STREAM_WRITE_ZEROES,
38
53
  POLL_POLLABLE_BLOCK,
54
+ POLL_POLLABLE_DISPOSE,
39
55
  POLL_POLLABLE_READY,
56
+ POLL_POLL_LIST,
40
57
  SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST,
58
+ SOCKET_RESOLVE_ADDRESS_SUBSCRIBE_REQUEST,
41
59
  SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST,
42
- SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST,
43
- FILE,
44
- HTTP,
45
- SOCKET,
60
+ SOCKET_RESOLVE_ADDRESS_TAKE_REQUEST,
61
+ SOCKET_GET_DEFAULT_RECEIVE_BUFFER_SIZE,
62
+ SOCKET_GET_DEFAULT_SEND_BUFFER_SIZE,
63
+ SOCKET_TCP_ACCEPT,
64
+ SOCKET_TCP_BIND_FINISH,
65
+ SOCKET_TCP_BIND_START,
66
+ SOCKET_TCP_CONNECT_FINISH,
67
+ SOCKET_TCP_CONNECT_START,
68
+ SOCKET_TCP_CREATE_HANDLE,
69
+ SOCKET_TCP_DISPOSE,
70
+ SOCKET_TCP_GET_LOCAL_ADDRESS,
71
+ SOCKET_TCP_GET_REMOTE_ADDRESS,
72
+ SOCKET_TCP_IS_LISTENING,
73
+ SOCKET_TCP_LISTEN_FINISH,
74
+ SOCKET_TCP_LISTEN_START,
75
+ SOCKET_TCP_SET_KEEP_ALIVE,
76
+ SOCKET_TCP_SET_LISTEN_BACKLOG_SIZE,
77
+ SOCKET_TCP_SHUTDOWN,
78
+ SOCKET_TCP_SUBSCRIBE,
79
+ SOCKET_UDP_BIND_FINISH,
80
+ SOCKET_UDP_BIND_START,
81
+ SOCKET_UDP_CREATE_HANDLE,
82
+ SOCKET_UDP_DISPOSE,
83
+ SOCKET_UDP_GET_LOCAL_ADDRESS,
84
+ SOCKET_UDP_GET_RECEIVE_BUFFER_SIZE,
85
+ SOCKET_UDP_GET_REMOTE_ADDRESS,
86
+ SOCKET_UDP_GET_SEND_BUFFER_SIZE,
87
+ SOCKET_UDP_GET_UNICAST_HOP_LIMIT,
88
+ SOCKET_UDP_SET_RECEIVE_BUFFER_SIZE,
89
+ SOCKET_UDP_SET_SEND_BUFFER_SIZE,
90
+ SOCKET_UDP_SET_UNICAST_HOP_LIMIT,
91
+ SOCKET_UDP_STREAM,
92
+ SOCKET_UDP_SUBSCRIBE,
93
+ SOCKET_INCOMING_DATAGRAM_STREAM_RECEIVE,
94
+ SOCKET_OUTGOING_DATAGRAM_STREAM_CHECK_SEND,
95
+ SOCKET_OUTGOING_DATAGRAM_STREAM_SEND,
96
+ SOCKET_DATAGRAM_STREAM_SUBSCRIBE,
97
+ SOCKET_DATAGRAM_STREAM_DISPOSE,
46
98
  STDERR,
47
99
  STDIN,
48
100
  STDOUT,
101
+ reverseMap,
49
102
  } from "./calls.js";
103
+ import {
104
+ SOCKET_STATE_BIND,
105
+ SOCKET_STATE_BOUND,
106
+ SOCKET_STATE_CONNECT,
107
+ SOCKET_STATE_CONNECTION,
108
+ SOCKET_STATE_LISTEN,
109
+ SOCKET_STATE_LISTENER,
110
+ socketResolveAddress,
111
+ getDefaultSendBufferSize,
112
+ getDefaultReceiveBufferSize,
113
+ } from "./worker-sockets.js";
114
+ import {
115
+ createTcpSocket,
116
+ socketTcpAccept,
117
+ socketTcpBindStart,
118
+ socketTcpConnectStart,
119
+ socketTcpDispose,
120
+ socketTcpFinish,
121
+ socketTcpGetLocalAddress,
122
+ socketTcpGetRemoteAddress,
123
+ socketTcpListenStart,
124
+ socketTcpSetKeepAlive,
125
+ socketTcpSetListenBacklogSize,
126
+ socketTcpShutdown,
127
+ tcpSockets,
128
+ } from "./worker-socket-tcp.js";
129
+ import {
130
+ createUdpSocket,
131
+ datagramStreams,
132
+ socketDatagramStreamDispose,
133
+ socketIncomingDatagramStreamReceive,
134
+ socketOutgoingDatagramStreamCheckSend,
135
+ socketOutgoingDatagramStreamSend,
136
+ socketUdpBindFinish,
137
+ socketUdpBindStart,
138
+ socketUdpDispose,
139
+ socketUdpGetLocalAddress,
140
+ socketUdpGetReceiveBufferSize,
141
+ socketUdpGetRemoteAddress,
142
+ socketUdpGetSendBufferSize,
143
+ socketUdpGetUnicastHopLimit,
144
+ socketUdpSetReceiveBufferSize,
145
+ socketUdpSetSendBufferSize,
146
+ socketUdpSetUnicastHopLimit,
147
+ socketUdpStream,
148
+ udpSockets,
149
+ } from "./worker-socket-udp.js";
50
150
 
51
- let streamCnt = 0,
52
- pollCnt = 0;
151
+ function log(msg) {
152
+ if (debug) process._rawDebug(msg);
153
+ }
53
154
 
54
- /** @type {Map<number, Promise<void>>} */
55
- export const unfinishedPolls = new Map();
155
+ let pollCnt = 0,
156
+ streamCnt = 0,
157
+ futureCnt = 0;
56
158
 
57
- /** @type {Map<number, { flushPromise: Promise<void> | null, stream: NodeJS.ReadableStream | NodeJS.WritableStream }>} */
58
- export const unfinishedStreams = new Map();
159
+ /**
160
+ * @typedef {{
161
+ * ready: bool,
162
+ * listener: () => void | null,
163
+ * polls: number[],
164
+ * parentStream: null | NodeJS.ReadableStream
165
+ * }} PollState
166
+ *
167
+ * @typedef {{
168
+ * stream: NodeJS.ReadableStream | NodeJS.WritableStream,
169
+ * flushPromise: Promise<void> | null,
170
+ * pollState
171
+ * }} Stream
172
+ *
173
+ * @typedef {{
174
+ * future: {
175
+ * tag: 'ok' | 'err',
176
+ * val: any,
177
+ * },
178
+ * pollState
179
+ * }} Future
180
+ */
59
181
 
60
- /** @type {Map<number, { value: any, error: bool }>} */
61
- export const unfinishedFutures = new Map();
182
+ /** @type {Map<number, PollState>} */
183
+ export const polls = new Map();
184
+
185
+ /** @type {Map<number, Stream>} */
186
+ export const streams = new Map();
187
+
188
+ /** @type {Map<number, Future>} */
189
+ export const futures = new Map();
190
+
191
+ export function createReadableStreamPollState(nodeStream) {
192
+ const pollState = {
193
+ ready: true,
194
+ listener: null,
195
+ polls: [],
196
+ parentStream: nodeStream,
197
+ };
198
+ function pollDone() {
199
+ pollStateReady(pollState);
200
+ nodeStream.off("end", pollDone);
201
+ nodeStream.off("close", pollDone);
202
+ nodeStream.off("error", pollDone);
203
+ }
204
+ nodeStream.on("end", pollDone);
205
+ nodeStream.on("close", pollDone);
206
+ nodeStream.on("error", pollDone);
207
+ return pollState;
208
+ }
62
209
 
63
210
  /**
64
211
  * @param {NodeJS.ReadableStream | NodeJS.WritableStream} stream
65
212
  */
66
- export function createStream(nodeStream, flushPromise) {
67
- unfinishedStreams.set(++streamCnt, {
68
- flushPromise,
213
+ export function createReadableStream(
214
+ nodeStream,
215
+ pollState = createReadableStreamPollState(nodeStream)
216
+ ) {
217
+ const stream = {
218
+ stream: nodeStream,
219
+ flushPromise: null,
220
+ pollState,
221
+ };
222
+ streams.set(++streamCnt, stream);
223
+ return streamCnt;
224
+ }
225
+
226
+ export function createWritableStream(nodeStream) {
227
+ const pollState = {
228
+ ready: true,
229
+ listener: null,
230
+ polls: [],
231
+ parentStream: null,
232
+ };
233
+ const stream = {
69
234
  stream: nodeStream,
70
- });
235
+ flushPromise: null,
236
+ pollState,
237
+ };
238
+ streams.set(++streamCnt, stream);
239
+ function pollReady() {
240
+ pollStateReady(pollState);
241
+ }
242
+ function pollDone() {
243
+ pollStateReady(pollState);
244
+ nodeStream.off("drain", pollReady);
245
+ nodeStream.off("finish", pollDone);
246
+ nodeStream.off("error", pollDone);
247
+ nodeStream.off("close", pollDone);
248
+ }
249
+ nodeStream.on("drain", pollReady);
250
+ nodeStream.on("finish", pollDone);
251
+ nodeStream.on("error", pollDone);
252
+ nodeStream.on("close", pollDone);
71
253
  return streamCnt;
72
254
  }
73
255
 
74
256
  // Stdio
75
- createStream(process.stdin, Promise.resolve());
76
- createStream(process.stdout, Promise.resolve());
77
- createStream(process.stderr, Promise.resolve());
257
+ // Stdin created when used
258
+ createWritableStream(stdout);
259
+ createWritableStream(stderr);
78
260
 
79
261
  /**
80
262
  * @param {number} streamId
81
263
  * @param {NodeJS.ReadableStream | NodeJS.WritableStream} stream
82
264
  */
83
- function streamError(streamId, stream, err) {
84
- if (stream.end) stream.end();
85
- // we delete the stream from unfinishedStreams as it is now "finished" (closed)
86
- unfinishedStreams.delete(streamId);
87
- return { tag: "last-operation-failed", val: err };
265
+ function streamError(err) {
266
+ return {
267
+ tag: "last-operation-failed",
268
+ val: { code: err.code, message: err.message, stack: err.stack },
269
+ };
88
270
  }
89
271
 
90
272
  /**
91
273
  * @param {number} streamId
92
- * @returns {{ stream: NodeJS.ReadableStream | NodeJS.WritableStream, flushPromise: Promise<void> | null }}
274
+ * @returns {{ stream: NodeJS.ReadableStream | NodeJS.WritableStream, polls: number[] }}
93
275
  */
94
276
  export function getStreamOrThrow(streamId) {
95
- const stream = unfinishedStreams.get(streamId);
277
+ if (!streamId) throw new Error("wasi-io trap: no stream id provided");
278
+ const stream = streams.get(streamId);
96
279
  // not in unfinished streams <=> closed
97
280
  if (!stream) throw { tag: "closed" };
98
- if (stream.stream.errored)
99
- throw streamError(streamId, stream, stream.stream.errored);
281
+ if (stream.stream.errored) throw streamError(stream.stream.errored);
100
282
  if (stream.stream.closed) {
101
- unfinishedStreams.delete(streamId);
102
283
  throw { tag: "closed" };
103
284
  }
104
285
  return stream;
105
286
  }
106
287
 
107
- function subscribeInstant(instant) {
108
- const duration = instant - hrtime.bigint();
109
- if (duration <= 0) return Promise.resolve();
110
- return new Promise((resolve) =>
111
- duration < 10e6
112
- ? setImmediate(resolve)
113
- : setTimeout(resolve, Number(duration) / 1e6)
114
- ).then(() => {
115
- if (hrtime.bigint() < instant) return subscribeInstant(instant);
116
- });
117
- }
118
-
119
288
  /**
120
289
  * @param {number} call
121
290
  * @param {number | null} id
@@ -123,111 +292,265 @@ function subscribeInstant(instant) {
123
292
  * @returns {Promise<any>}
124
293
  */
125
294
  function handle(call, id, payload) {
295
+ if (uncaughtException) throw uncaughtException;
126
296
  switch (call) {
127
297
  // Http
128
298
  case HTTP_CREATE_REQUEST: {
129
- const { method, url, headers, body } = payload;
130
- return createFuture(createHttpRequest(method, url, headers, body));
299
+ const {
300
+ method,
301
+ scheme,
302
+ authority,
303
+ pathWithQuery,
304
+ headers,
305
+ body,
306
+ connectTimeout,
307
+ betweenBytesTimeout,
308
+ firstByteTimeout,
309
+ } = payload;
310
+ return createFuture(
311
+ createHttpRequest(
312
+ method,
313
+ scheme,
314
+ authority,
315
+ pathWithQuery,
316
+ headers,
317
+ body,
318
+ connectTimeout,
319
+ betweenBytesTimeout,
320
+ firstByteTimeout
321
+ )
322
+ );
131
323
  }
132
324
  case OUTPUT_STREAM_CREATE | HTTP: {
133
- const webTransformStream = new TransformStream();
134
- const stream = Writable.fromWeb(webTransformStream.writable);
325
+ const stream = new PassThrough();
135
326
  // content length is passed as payload
327
+ stream.contentLength = payload;
136
328
  stream.bytesRemaining = payload;
137
- stream.readableBodyStream = webTransformStream.readable;
138
- return createStream(stream);
329
+ return createWritableStream(stream);
139
330
  }
140
331
  case OUTPUT_STREAM_SUBSCRIBE | HTTP:
141
332
  case OUTPUT_STREAM_FLUSH | HTTP:
142
333
  case OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH | HTTP: {
143
334
  // http flush is a noop
144
335
  const { stream } = getStreamOrThrow(id);
145
- // this existing indicates it's still unattached
146
- // therefore there is no subscribe or backpressure
147
- if (stream.readableBodyStream) {
148
- switch (call) {
149
- case OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH | HTTP:
150
- return handle(OUTPUT_STREAM_WRITE | HTTP, id, payload);
151
- case OUTPUT_STREAM_FLUSH | HTTP:
152
- return;
153
- case OUTPUT_STREAM_SUBSCRIBE | HTTP:
154
- return 0;
336
+ if (call === (OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH | HTTP)) {
337
+ stream.bytesRemaining -= payload.byteLength;
338
+ if (stream.bytesRemaining < 0) {
339
+ throw {
340
+ tag: "last-operation-failed",
341
+ val: {
342
+ tag: "HTTP-request-body-size",
343
+ val: stream.contentLength - stream.bytesRemaining,
344
+ },
345
+ };
155
346
  }
156
347
  }
157
- if (call === (OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH | HTTP))
158
- stream.bytesRemaining -= payload.byteLength;
159
348
  // otherwise fall through to generic implementation
160
349
  return handle(call & ~HTTP, id, payload);
161
350
  }
162
- case OUTPUT_STREAM_DISPOSE | HTTP:
163
- throw new Error('Internal error: HTTP output stream dispose is bypassed for FINISH');
164
351
  case OUTPUT_STREAM_WRITE | HTTP: {
165
352
  const { stream } = getStreamOrThrow(id);
166
353
  stream.bytesRemaining -= payload.byteLength;
167
354
  if (stream.bytesRemaining < 0) {
168
355
  throw {
169
- tag: 'last-operation-failed',
170
- val: 'too much written to output stream'
356
+ tag: "last-operation-failed",
357
+ val: {
358
+ tag: "HTTP-request-body-size",
359
+ val: stream.contentLength - stream.bytesRemaining,
360
+ },
171
361
  };
172
362
  }
173
363
  const output = handle(OUTPUT_STREAM_WRITE, id, payload);
174
364
  return output;
175
365
  }
366
+ case OUTPUT_STREAM_DISPOSE | HTTP:
367
+ throw new Error(
368
+ "wasi-io trap: Output stream dispose not implemented as an IO-call for HTTP"
369
+ );
176
370
  case HTTP_OUTPUT_STREAM_FINISH: {
177
- const { stream } = getStreamOrThrow(id);
371
+ let stream;
372
+ try {
373
+ ({ stream } = getStreamOrThrow(id));
374
+ } catch (e) {
375
+ if (e.tag === "closed")
376
+ throw { tag: "internal-error", val: "stream closed" };
377
+ if (e.tag === "last-operation-failed")
378
+ throw { tag: "internal-error", val: e.val.message };
379
+ }
178
380
  if (stream.bytesRemaining > 0) {
179
381
  throw {
180
- tag: 'internal-error',
181
- val: 'not enough written to body stream'
382
+ tag: "HTTP-request-body-size",
383
+ val: stream.contentLength - stream.bytesRemaining,
182
384
  };
183
385
  }
184
386
  if (stream.bytesRemaining < 0) {
185
387
  throw {
186
- tag: 'internal-error',
187
- val: 'too much written to body stream'
388
+ tag: "HTTP-request-body-size",
389
+ val: stream.contentLength - stream.bytesRemaining,
188
390
  };
189
391
  }
190
392
  stream.end();
191
- break;
393
+ return;
192
394
  }
395
+ case HTTP_OUTGOING_BODY_DISPOSE:
396
+ if (!streams.delete(id))
397
+ throw new Error("wasi-io trap: stream not found to dispose");
398
+ return;
399
+ case HTTP_SERVER_START:
400
+ return startHttpServer(id, payload);
401
+ case HTTP_SERVER_STOP:
402
+ return stopHttpServer(id);
403
+ case HTTP_SERVER_SET_OUTGOING_RESPONSE:
404
+ return setOutgoingResponse(id, payload);
405
+ case HTTP_SERVER_CLEAR_OUTGOING_RESPONSE:
406
+ return clearOutgoingResponse(id);
193
407
 
194
- // Sockets
408
+ // Sockets name resolution
195
409
  case SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST:
196
- return createFuture(resolve(payload.hostname));
410
+ return createFuture(socketResolveAddress(payload));
411
+ case SOCKET_RESOLVE_ADDRESS_SUBSCRIBE_REQUEST:
412
+ return createPoll(futures.get(id).pollState);
197
413
  case SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST:
198
- return void unfinishedFutures.delete(id);
199
- case SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST: {
200
- const future = unfinishedFutures.get(id);
201
- if (!future) {
202
- // future not ready yet
203
- if (unfinishedPolls.get(id)) {
204
- throw 'would-block';
205
- }
206
- throw new Error("future already got and dropped");
207
- }
208
- unfinishedFutures.delete(id);
209
- return future;
210
- }
211
- case OUTPUT_STREAM_CREATE | SOCKET: {
212
- // TODO: implement
213
- }
214
- case INPUT_STREAM_CREATE | SOCKET: {
215
- // TODO: implement
414
+ return void futureDispose(id, true);
415
+ case SOCKET_RESOLVE_ADDRESS_TAKE_REQUEST: {
416
+ const val = futureTakeValue(id);
417
+ if (val === undefined) throw "would-block";
418
+ // double take avoidance is ensured
419
+ return val.val;
216
420
  }
217
421
 
422
+ // Sockets TCP
423
+ case SOCKET_TCP_ACCEPT:
424
+ return socketTcpAccept(id);
425
+ case SOCKET_TCP_CREATE_HANDLE:
426
+ return createTcpSocket();
427
+ case SOCKET_TCP_BIND_START:
428
+ return socketTcpBindStart(id, payload.localAddress, payload.family);
429
+ case SOCKET_TCP_BIND_FINISH:
430
+ return socketTcpFinish(id, SOCKET_STATE_BIND, SOCKET_STATE_BOUND);
431
+ case SOCKET_TCP_CONNECT_START:
432
+ return socketTcpConnectStart(id, payload.remoteAddress, payload.family);
433
+ case SOCKET_TCP_CONNECT_FINISH:
434
+ return socketTcpFinish(id, SOCKET_STATE_CONNECT, SOCKET_STATE_CONNECTION);
435
+ case SOCKET_TCP_LISTEN_START:
436
+ return socketTcpListenStart(id);
437
+ case SOCKET_TCP_LISTEN_FINISH:
438
+ return socketTcpFinish(id, SOCKET_STATE_LISTEN, SOCKET_STATE_LISTENER);
439
+ case SOCKET_TCP_IS_LISTENING:
440
+ return tcpSockets.get(id).state === SOCKET_STATE_LISTENER;
441
+ case SOCKET_GET_DEFAULT_SEND_BUFFER_SIZE:
442
+ return getDefaultSendBufferSize(id);
443
+ case SOCKET_GET_DEFAULT_RECEIVE_BUFFER_SIZE:
444
+ return getDefaultReceiveBufferSize(id);
445
+ case SOCKET_TCP_SET_LISTEN_BACKLOG_SIZE:
446
+ return socketTcpSetListenBacklogSize(id);
447
+ case SOCKET_TCP_GET_LOCAL_ADDRESS:
448
+ return socketTcpGetLocalAddress(id);
449
+ case SOCKET_TCP_GET_REMOTE_ADDRESS:
450
+ return socketTcpGetRemoteAddress(id);
451
+ case SOCKET_TCP_SHUTDOWN:
452
+ return socketTcpShutdown(id, payload);
453
+ case SOCKET_TCP_SUBSCRIBE:
454
+ return createPoll(tcpSockets.get(id).pollState);
455
+ case SOCKET_TCP_SET_KEEP_ALIVE:
456
+ return socketTcpSetKeepAlive(id, payload);
457
+ case SOCKET_TCP_DISPOSE:
458
+ return socketTcpDispose(id);
459
+
460
+ // Sockets UDP
461
+ case SOCKET_UDP_CREATE_HANDLE:
462
+ return createUdpSocket(payload);
463
+ case SOCKET_UDP_BIND_START:
464
+ return socketUdpBindStart(id, payload.localAddress, payload.family);
465
+ case SOCKET_UDP_BIND_FINISH:
466
+ return socketUdpBindFinish(id);
467
+ case SOCKET_UDP_STREAM:
468
+ return socketUdpStream(id, payload);
469
+ case SOCKET_UDP_SUBSCRIBE:
470
+ return createPoll(udpSockets.get(id).pollState);
471
+ case SOCKET_UDP_GET_LOCAL_ADDRESS:
472
+ return socketUdpGetLocalAddress(id);
473
+ case SOCKET_UDP_GET_REMOTE_ADDRESS:
474
+ return socketUdpGetRemoteAddress(id);
475
+ case SOCKET_UDP_SET_RECEIVE_BUFFER_SIZE:
476
+ return socketUdpSetReceiveBufferSize(id, payload);
477
+ case SOCKET_UDP_SET_SEND_BUFFER_SIZE:
478
+ return socketUdpSetSendBufferSize(id, payload);
479
+ case SOCKET_UDP_SET_UNICAST_HOP_LIMIT:
480
+ return socketUdpSetUnicastHopLimit(id, payload);
481
+ case SOCKET_UDP_GET_RECEIVE_BUFFER_SIZE:
482
+ return socketUdpGetReceiveBufferSize(id);
483
+ case SOCKET_UDP_GET_SEND_BUFFER_SIZE:
484
+ return socketUdpGetSendBufferSize(id);
485
+ case SOCKET_UDP_GET_UNICAST_HOP_LIMIT:
486
+ return socketUdpGetUnicastHopLimit(id);
487
+ case SOCKET_UDP_DISPOSE:
488
+ return socketUdpDispose(id);
489
+
490
+ case SOCKET_INCOMING_DATAGRAM_STREAM_RECEIVE:
491
+ return socketIncomingDatagramStreamReceive(id, payload);
492
+ case SOCKET_OUTGOING_DATAGRAM_STREAM_CHECK_SEND:
493
+ return socketOutgoingDatagramStreamCheckSend(id);
494
+ case SOCKET_OUTGOING_DATAGRAM_STREAM_SEND:
495
+ return socketOutgoingDatagramStreamSend(id, payload);
496
+ case SOCKET_DATAGRAM_STREAM_SUBSCRIBE:
497
+ return createPoll(datagramStreams.get(id).pollState);
498
+ case SOCKET_DATAGRAM_STREAM_DISPOSE:
499
+ return socketDatagramStreamDispose(id);
500
+
218
501
  // Stdio
502
+ case OUTPUT_STREAM_BLOCKING_FLUSH | STDOUT:
503
+ case OUTPUT_STREAM_BLOCKING_FLUSH | STDERR:
504
+ // no blocking flush for stdio in Node.js
505
+ return;
219
506
  case OUTPUT_STREAM_DISPOSE | STDOUT:
220
507
  case OUTPUT_STREAM_DISPOSE | STDERR:
221
- case INPUT_STREAM_DISPOSE | STDIN:
222
508
  return;
509
+ case INPUT_STREAM_CREATE | STDIN: {
510
+ return createReadableStream(
511
+ new Readable({
512
+ read(n) {
513
+ if (n <= 0) return void this.push(null);
514
+ let buf = Buffer.allocUnsafeSlow(n);
515
+ read(0, buf, 0, n, null, (err, bytesRead) => {
516
+ if (err) {
517
+ if (err.code === "EAGAIN") {
518
+ nextTick(() => void this._read(n));
519
+ return;
520
+ }
521
+ this.destroy(err);
522
+ } else if (bytesRead > 0) {
523
+ if (bytesRead !== buf.length) {
524
+ const dst = Buffer.allocUnsafeSlow(bytesRead);
525
+ buf.copy(dst, 0, 0, bytesRead);
526
+ buf = dst;
527
+ }
528
+ this.push(buf);
529
+ } else {
530
+ this.push(null);
531
+ }
532
+ });
533
+ },
534
+ })
535
+ );
536
+ }
223
537
 
224
538
  // Clocks
225
539
  case CLOCKS_NOW:
226
540
  return hrtime.bigint();
227
541
  case CLOCKS_DURATION_SUBSCRIBE:
228
- return createPoll(subscribeInstant(hrtime.bigint() + payload));
229
- case CLOCKS_INSTANT_SUBSCRIBE:
230
- return createPoll(subscribeInstant(payload));
542
+ payload = hrtime.bigint() + payload;
543
+ // fallthrough
544
+ case CLOCKS_INSTANT_SUBSCRIBE: {
545
+ const pollState = {
546
+ ready: false,
547
+ listener: null,
548
+ polls: [],
549
+ parentStream: null,
550
+ };
551
+ subscribeInstant(pollState, payload);
552
+ return createPoll(pollState);
553
+ }
231
554
 
232
555
  // Filesystem
233
556
  case INPUT_STREAM_CREATE | FILE: {
@@ -238,9 +561,7 @@ function handle(call, id, payload) {
238
561
  highWaterMark: 64 * 1024,
239
562
  start: Number(offset),
240
563
  });
241
- // for some reason fs streams dont emit readable on end
242
- stream.on("end", () => void stream.emit("readable"));
243
- return createStream(stream);
564
+ return createReadableStream(stream);
244
565
  }
245
566
  case OUTPUT_STREAM_CREATE | FILE: {
246
567
  const { fd, offset } = payload;
@@ -251,319 +572,392 @@ function handle(call, id, payload) {
251
572
  highWaterMark: 64 * 1024,
252
573
  start: Number(offset),
253
574
  });
254
- return createStream(stream);
575
+ return createWritableStream(stream);
255
576
  }
256
- // Generic call implementations (streams + polls)
257
- default:
258
- switch (call & CALL_MASK) {
259
- case INPUT_STREAM_READ: {
260
- const { stream } = getStreamOrThrow(id);
261
- const res = stream.read(Number(payload));
262
- return res ?? new Uint8Array();
263
- }
264
- case INPUT_STREAM_BLOCKING_READ:
265
- return Promise.resolve(
266
- unfinishedPolls.get(
267
- handle(INPUT_STREAM_SUBSCRIBE | (call & CALL_TYPE_MASK), id)
268
- )
269
- ).then(() =>
270
- handle(INPUT_STREAM_READ | (call & CALL_TYPE_MASK), id, payload)
271
- );
272
- case INPUT_STREAM_SKIP:
273
- return handle(
274
- INPUT_STREAM_READ | (call & CALL_TYPE_MASK),
275
- id,
276
- new Uint8Array(Number(payload))
277
- );
278
- case INPUT_STREAM_BLOCKING_SKIP:
279
- return handle(
280
- INPUT_STREAM_BLOCKING_READ | (call & CALL_TYPE_MASK),
281
- id,
282
- new Uint8Array(Number(payload))
283
- );
284
- case INPUT_STREAM_SUBSCRIBE: {
285
- const stream = unfinishedStreams.get(id)?.stream;
286
- if (id === 1) {
287
- // TODO: stdin subscribe
288
- return 0;
289
- }
290
- // already closed or errored -> immediately return poll
291
- // (poll 0 is immediately resolved)
292
- if (
293
- !stream ||
294
- stream.closed ||
295
- stream.errored ||
296
- stream.readableLength > 0
297
- )
298
- return 0;
299
- let resolve, reject;
300
- return createPoll(
301
- new Promise((_resolve, _reject) => {
302
- stream
303
- .once("readable", (resolve = _resolve))
304
- .once("error", (reject = _reject));
305
- }).then(
306
- () => void stream.off("error", reject),
307
- (err) => {
308
- stream.off("readable", resolve);
309
- throw streamError(id, stream.stream, err);
310
- }
311
- )
312
- );
313
- }
314
- case INPUT_STREAM_DISPOSE:
315
- unfinishedStreams.delete(id);
316
- return;
577
+ }
317
578
 
318
- case OUTPUT_STREAM_CHECK_WRITE: {
319
- const { stream } = getStreamOrThrow(id);
320
- return BigInt(stream.writableHighWaterMark - stream.writableLength);
321
- }
322
- case OUTPUT_STREAM_WRITE: {
323
- const { stream } = getStreamOrThrow(id);
324
- if (
325
- payload.byteLength >
326
- stream.writableHighWaterMark - stream.writableLength
579
+ // Generic call implementations (streams + polls)
580
+ switch (call & CALL_MASK) {
581
+ case INPUT_STREAM_READ: {
582
+ const stream = getStreamOrThrow(id);
583
+ if (!stream.pollState.ready) return new Uint8Array();
584
+ const res = stream.stream.read(
585
+ Math.min(stream.stream.readableLength, Number(payload))
586
+ );
587
+ if (res) return res;
588
+ if (stream.stream.readableEnded) return { tag: "closed" };
589
+ return new Uint8Array();
590
+ }
591
+ case INPUT_STREAM_BLOCKING_READ: {
592
+ const { pollState } = streams.get(id);
593
+ pollStateCheck(pollState);
594
+ if (pollState.ready)
595
+ return handle(INPUT_STREAM_READ | (call & CALL_TYPE_MASK), id, payload);
596
+ return new Promise((resolve) => void (pollState.listener = resolve)).then(
597
+ () => handle(INPUT_STREAM_READ | (call & CALL_TYPE_MASK), id, payload)
598
+ );
599
+ }
600
+ case INPUT_STREAM_SKIP:
601
+ return handle(
602
+ INPUT_STREAM_READ | (call & CALL_TYPE_MASK),
603
+ id,
604
+ new Uint8Array(Number(payload))
605
+ );
606
+ case INPUT_STREAM_BLOCKING_SKIP:
607
+ return handle(
608
+ INPUT_STREAM_BLOCKING_READ | (call & CALL_TYPE_MASK),
609
+ id,
610
+ new Uint8Array(Number(payload))
611
+ );
612
+ case INPUT_STREAM_SUBSCRIBE:
613
+ return createPoll(streams.get(id).pollState);
614
+ case INPUT_STREAM_DISPOSE: {
615
+ const stream = streams.get(id);
616
+ verifyPollsDroppedForDrop(stream.pollState, "input stream");
617
+ streams.delete(id);
618
+ return;
619
+ }
620
+ case OUTPUT_STREAM_CHECK_WRITE: {
621
+ const { stream, pollState } = getStreamOrThrow(id);
622
+ const bytes = stream.writableHighWaterMark - stream.writableLength;
623
+ if (bytes === 0) pollState.ready = false;
624
+ return BigInt(bytes);
625
+ }
626
+ case OUTPUT_STREAM_WRITE: {
627
+ const { stream } = getStreamOrThrow(id);
628
+ if (
629
+ payload.byteLength >
630
+ stream.writableHighWaterMark - stream.writableLength
631
+ )
632
+ throw new Error("wasi-io trap: attempt to write too many bytes");
633
+ return void stream.write(payload);
634
+ }
635
+ case OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH: {
636
+ const stream = getStreamOrThrow(id);
637
+ // if an existing flush, try again after that
638
+ if (stream.flushPromise)
639
+ return stream.flushPromise.then(() => handle(call, id, payload));
640
+ if (
641
+ payload.byteLength >
642
+ stream.stream.writableHighWaterMark - stream.stream.writableLength
643
+ ) {
644
+ new Error(
645
+ "wasi-io trap: Cannot write more than permitted writable length"
646
+ );
647
+ }
648
+ stream.pollState.ready = false;
649
+ return (stream.flushPromise = new Promise((resolve, reject) => {
650
+ stream.stream.write(payload, (err) => {
651
+ stream.flushPromise = null;
652
+ pollStateReady(stream.pollState);
653
+ if (err) return void reject(streamError(err));
654
+ resolve(BigInt(payload.byteLength));
655
+ });
656
+ }));
657
+ }
658
+ case OUTPUT_STREAM_FLUSH: {
659
+ const stream = getStreamOrThrow(id);
660
+ if (stream.flushPromise) return;
661
+ stream.pollState.ready = false;
662
+ return (stream.flushPromise = new Promise((resolve, reject) => {
663
+ stream.stream.write(new Uint8Array([]), (err) => {
664
+ stream.flushPromise = null;
665
+ pollStateReady(stream.pollState);
666
+ if (err) return void reject(streamError(err));
667
+ resolve();
668
+ });
669
+ }));
670
+ }
671
+ case OUTPUT_STREAM_BLOCKING_FLUSH: {
672
+ const stream = getStreamOrThrow(id);
673
+ if (stream.flushPromise) return stream.flushPromise;
674
+ return new Promise((resolve, reject) => {
675
+ stream.stream.write(new Uint8Array([]), (err) =>
676
+ err ? reject(streamError(err)) : resolve()
677
+ );
678
+ });
679
+ }
680
+ case OUTPUT_STREAM_WRITE_ZEROES:
681
+ return handle(
682
+ OUTPUT_STREAM_WRITE | (call & CALL_TYPE_MASK),
683
+ id,
684
+ new Uint8Array(Number(payload))
685
+ );
686
+ case OUTPUT_STREAM_BLOCKING_WRITE_ZEROES_AND_FLUSH:
687
+ return handle(
688
+ OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH | (call & CALL_TYPE_MASK),
689
+ id,
690
+ new Uint8Array(Number(payload))
691
+ );
692
+ case OUTPUT_STREAM_SPLICE: {
693
+ const outputStream = getStreamOrThrow(id);
694
+ const inputStream = getStreamOrThrow(payload.src);
695
+ let bytesRemaining = Number(payload.len);
696
+ let chunk;
697
+ while (
698
+ bytesRemaining > 0 &&
699
+ (chunk = inputStream.stream.read(
700
+ Math.min(
701
+ outputStream.writableHighWaterMark - outputStream.writableLength,
702
+ bytesRemaining
327
703
  )
328
- throw new Error("wasi-io error: attempt to write too many bytes");
329
- return void stream.write(payload);
330
- }
331
- case OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH: {
332
- const { stream, flushPromise } = getStreamOrThrow(id);
333
- // if an existing flush, try again after that
334
- if (flushPromise)
335
- return flushPromise.then(() => handle(call, id, payload));
336
- if (
337
- payload.byteLength >
338
- stream.writableHighWaterMark - stream.writableLength
339
- ) {
340
- throw streamError(
341
- id,
342
- stream,
343
- new Error("Cannot write more than permitted writable length")
344
- );
704
+ ))
705
+ ) {
706
+ bytesRemaining -= chunk.byteLength;
707
+ outputStream.stream.write(chunk);
708
+ }
709
+ if (inputStream.stream.errored)
710
+ throw streamError(inputStream.stream.errored);
711
+ if (outputStream.stream.errored)
712
+ throw streamError(outputStream.stream.errored);
713
+ return payload.len - BigInt(bytesRemaining);
714
+ }
715
+ case OUTPUT_STREAM_SUBSCRIBE:
716
+ return createPoll(streams.get(id).pollState);
717
+ case OUTPUT_STREAM_BLOCKING_SPLICE: {
718
+ const outputStream = getStreamOrThrow(id);
719
+ let promise = Promise.resolve();
720
+ let resolve, reject;
721
+ if (outputStream.stream.writableNeedDrain) {
722
+ promise = new Promise((_resolve, _reject) => {
723
+ outputStream.stream
724
+ .once("drain", (resolve = _resolve))
725
+ .once("error", (reject = _reject));
726
+ }).then(
727
+ () => {
728
+ outputStream.stream.off("error", reject);
729
+ },
730
+ (err) => {
731
+ outputStream.stream.off("drain", resolve);
732
+ throw streamError(err);
345
733
  }
346
- return new Promise((resolve, reject) => {
347
- stream.write(payload, (err) => {
348
- if (err) return void reject(streamError(id, stream, err));
349
- resolve(BigInt(payload.byteLength));
350
- });
351
- });
352
- }
353
- case OUTPUT_STREAM_FLUSH: {
354
- const stream = getStreamOrThrow(id);
355
- if (stream.flushPromise) return;
356
- return (stream.flushPromise = new Promise((resolve, reject) => {
357
- stream.stream.write(new Uint8Array([]), (err) =>
358
- err ? reject(streamError(id, stream.stream, err)) : resolve()
359
- );
734
+ );
735
+ }
736
+ const inputStream = getStreamOrThrow(payload.src);
737
+ if (!inputStream.stream.readable) {
738
+ promise = promise.then(() =>
739
+ new Promise((_resolve, _reject) => {
740
+ inputStream.stream
741
+ .once("readable", (resolve = _resolve))
742
+ .once("error", (reject = _reject));
360
743
  }).then(
361
- () => void (stream.stream.flushPromise = null),
744
+ () => {
745
+ inputStream.stream.off("error", reject);
746
+ },
362
747
  (err) => {
363
- stream.stream.flushPromise = null;
364
- throw streamError(id, stream.stream, err);
748
+ inputStream.stream.off("readable", resolve);
749
+ throw streamError(err);
365
750
  }
366
- ));
367
- }
368
- case OUTPUT_STREAM_BLOCKING_FLUSH: {
369
- const { stream, flushPromise } = getStreamOrThrow(id);
370
- if (flushPromise) return flushPromise;
371
- return new Promise((resolve, reject) => {
372
- stream.write(new Uint8Array([]), (err) =>
373
- err ? reject(streamError(id, stream, err)) : resolve()
374
- );
375
- });
376
- }
377
- case OUTPUT_STREAM_WRITE_ZEROES:
378
- return handle(
379
- OUTPUT_STREAM_WRITE | (call & CALL_TYPE_MASK),
380
- id,
381
- new Uint8Array(Number(payload))
382
- );
383
- case OUTPUT_STREAM_BLOCKING_WRITE_ZEROES_AND_FLUSH:
384
- return handle(
385
- OUTPUT_STREAM_BLOCKING_WRITE_AND_FLUSH | (call & CALL_TYPE_MASK),
386
- id,
387
- new Uint8Array(Number(payload))
388
- );
389
- case OUTPUT_STREAM_SPLICE: {
390
- const { stream: outputStream } = getStreamOrThrow(id);
391
- const { stream: inputStream } = getStreamOrThrow(payload.src);
392
- let bytesRemaining = Number(payload.len);
393
- let chunk;
394
- while (
395
- bytesRemaining > 0 &&
396
- (chunk = inputStream.read(
397
- Math.min(
398
- outputStream.writableHighWaterMark -
399
- outputStream.writableLength,
400
- bytesRemaining
401
- )
402
- ))
403
- ) {
404
- bytesRemaining -= chunk.byteLength;
405
- outputStream.write(chunk);
406
- }
407
- // TODO: these error handlers should be attached, and only for the duration of the splice flush
408
- if (inputStream.errored)
409
- throw streamError(payload.src, inputStream, inputStream.errored);
410
- if (outputStream.errored)
411
- throw streamError(id, outputStream, outputStream.errored);
412
- return payload.len - BigInt(bytesRemaining);
413
- }
414
- case OUTPUT_STREAM_SUBSCRIBE: {
415
- const { stream, flushPromise } = unfinishedStreams.get(id) ?? {};
416
- if (flushPromise) return flushPromise;
417
- // not added to unfinishedPolls => it's an immediately resolved poll
418
- if (
419
- !stream ||
420
- stream.closed ||
421
- stream.errored ||
422
- !stream.writableNeedDrain
423
751
  )
424
- return 0;
425
- let resolve, reject;
426
- return createPoll(
427
- new Promise((_resolve, _reject) => {
428
- stream
429
- .once("drain", (resolve = _resolve))
430
- .once("error", (reject = _reject));
431
- }).then(() => void stream.off("error", reject)),
432
- (err) => {
433
- stream.off("drain", resolve);
434
- throw streamError(id, stream, err);
435
- }
436
- );
437
- }
438
- case OUTPUT_STREAM_BLOCKING_SPLICE: {
439
- const { stream: outputStream } = getStreamOrThrow(id);
440
- let promise = Promise.resolve();
441
- let resolve, reject;
442
- if (outputStream.writableNeedDrain) {
443
- promise = new Promise((_resolve, _reject) => {
444
- outputStream
445
- .once("drain", (resolve = _resolve))
446
- .once("error", (reject = _reject));
447
- }).then(
448
- () => {
449
- outputStream.off("error", reject);
450
- },
451
- (err) => {
452
- outputStream.off("drain", resolve);
453
- throw streamError(err);
454
- }
455
- );
456
- }
457
- const { stream: inputStream } = getStreamOrThrow(payload.src);
458
- if (!inputStream.readable) {
459
- promise = promise.then(() =>
460
- new Promise((_resolve, _reject) => {
461
- inputStream
462
- .once("readable", (resolve = _resolve))
463
- .once("error", (reject = _reject));
464
- }).then(
465
- () => {
466
- inputStream.off("error", reject);
467
- },
468
- (err) => {
469
- inputStream.off("readable", resolve);
470
- throw streamError(err);
471
- }
472
- )
473
- );
474
- }
475
- return promise.then(() => handle(OUTPUT_STREAM_SPLICE, id, payload));
476
- }
477
- case OUTPUT_STREAM_DISPOSE: {
478
- const stream = unfinishedStreams.get(id);
479
- if (stream) {
480
- stream.stream.end();
481
- unfinishedStreams.delete(id);
482
- }
483
- return;
484
- }
752
+ );
753
+ }
754
+ return promise.then(() => handle(OUTPUT_STREAM_SPLICE, id, payload));
755
+ }
756
+ case OUTPUT_STREAM_DISPOSE: {
757
+ const stream = streams.get(id);
758
+ verifyPollsDroppedForDrop(stream.pollState, "output stream");
759
+ stream.stream.end();
760
+ streams.delete(id);
761
+ return;
762
+ }
485
763
 
486
- case POLL_POLLABLE_READY:
487
- return !unfinishedPolls.has(id);
488
- case POLL_POLLABLE_BLOCK:
489
- payload = [id];
490
- // [intentional case fall-through]
491
- case POLL_POLL_LIST: {
492
- const resolvedList = payload.filter((id) => !unfinishedPolls.has(id));
493
- if (resolvedList.length > 0) return resolvedList;
494
- // if all polls are promise type, we just race them
495
- return Promise.race(
496
- payload.map((id) => unfinishedPolls.get(id))
497
- ).then(() => {
498
- const resolvedList = payload.filter(
499
- (id) => !unfinishedPolls.has(id)
500
- );
501
- if (resolvedList.length === 0)
502
- throw new Error("poll promise did not unregister poll");
503
- return resolvedList;
504
- });
764
+ case POLL_POLLABLE_READY:
765
+ return polls.get(id).ready;
766
+ case POLL_POLLABLE_BLOCK:
767
+ payload = [id];
768
+ // [intentional case fall-through]
769
+ case POLL_POLL_LIST: {
770
+ if (payload.length === 0)
771
+ throw new Error("wasi-io trap: attempt to poll on empty list");
772
+ const doneList = [];
773
+ const pollList = payload.map((pollId) => polls.get(pollId));
774
+ for (const [idx, pollState] of pollList.entries()) {
775
+ pollStateCheck(pollState);
776
+ if (pollState.ready) doneList.push(idx);
777
+ }
778
+ if (doneList.length > 0) return new Uint32Array(doneList);
779
+ let readyPromiseResolve;
780
+ const readyPromise = new Promise(
781
+ (resolve) => void (readyPromiseResolve = resolve)
782
+ );
783
+ for (const poll of pollList) {
784
+ poll.listener = readyPromiseResolve;
785
+ }
786
+ return readyPromise.then(() => {
787
+ for (const [idx, pollState] of pollList.entries()) {
788
+ pollState.listener = null;
789
+ if (pollState.ready) doneList.push(idx);
505
790
  }
791
+ return new Uint32Array(doneList);
792
+ });
793
+ }
794
+ case POLL_POLLABLE_DISPOSE:
795
+ if (!polls.delete(id))
796
+ throw new Error(
797
+ `wasi-io trap: Disposed a poll ${id} that does not exist`
798
+ );
799
+ return;
506
800
 
507
- case FUTURE_GET_VALUE_AND_DISPOSE: {
508
- const future = unfinishedFutures.get(id);
509
- if (!future) {
510
- // future not ready yet
511
- if (unfinishedPolls.get(id)) {
512
- // if ((call & CALL_TYPE_MASK) ===
513
- // http futures throw
514
- throw undefined;
515
- }
516
- throw new Error("future already got and dropped");
517
- }
518
- unfinishedFutures.delete(id);
519
- return future;
520
- }
521
- case FUTURE_DISPOSE:
522
- return void unfinishedFutures.delete(id);
523
-
524
- default:
525
- throw new Error(
526
- `Unknown call ${(call & CALL_MASK) >> CALL_SHIFT} with type ${
527
- call & CALL_TYPE_MASK
528
- }`
529
- );
530
- }
801
+ case FUTURE_TAKE_VALUE:
802
+ return futureTakeValue(id);
803
+
804
+ case FUTURE_SUBSCRIBE: {
805
+ const { pollState } = futures.get(id);
806
+ const pollId = ++pollCnt;
807
+ polls.set(pollId, pollState);
808
+ return pollId;
809
+ }
810
+ case FUTURE_DISPOSE:
811
+ return void futureDispose(id, true);
812
+ default:
813
+ throw new Error(
814
+ `wasi-io trap: Unknown call ${call} (${reverseMap[call]}) with type ${
815
+ reverseMap[call & CALL_TYPE_MASK]
816
+ }`
817
+ );
531
818
  }
532
819
  }
533
820
 
534
- // poll promises must always resolve and never error
535
- export function createPoll(promise) {
821
+ /**
822
+ * @param {PollState} pollState
823
+ */
824
+ function createPoll(pollState) {
536
825
  const pollId = ++pollCnt;
537
- unfinishedPolls.set(
538
- pollId,
539
- promise.then(
540
- () => void unfinishedPolls.delete(pollId),
541
- (err) => {
542
- process._rawDebug("Unexpected poll error");
543
- process._rawDebug(err);
544
- process.exit(1);
545
- }
546
- )
547
- );
826
+ pollState.polls.push(pollId);
827
+ polls.set(pollId, pollState);
548
828
  return pollId;
549
829
  }
550
830
 
551
- export function createFuture(promise) {
552
- const pollId = ++pollCnt;
553
- unfinishedPolls.set(
554
- pollId,
555
- promise.then(
556
- (value) => {
557
- unfinishedPolls.delete(pollId);
558
- unfinishedFutures.set(pollId, { value, error: false });
559
- },
560
- (value) => {
561
- unfinishedPolls.delete(pollId);
562
- unfinishedFutures.set(pollId, { value, error: true });
563
- }
564
- )
831
+ function subscribeInstant(pollState, instant) {
832
+ const duration = instant - hrtime.bigint();
833
+ if (duration <= 0) return pollStateReady(pollState);
834
+ function cb() {
835
+ if (hrtime.bigint() < instant) return subscribeInstant(pollState, instant);
836
+ pollStateReady(pollState);
837
+ }
838
+ if (duration < 10e6) setImmediate(cb);
839
+ else setTimeout(cb, Number(duration) / 1e6);
840
+ }
841
+
842
+ /**
843
+ * @param {PollState} pollState
844
+ * @param {string} polledResourceDebugName
845
+ */
846
+ export function verifyPollsDroppedForDrop(pollState, polledResourceDebugName) {
847
+ for (const pollId of pollState.polls) {
848
+ const poll = polls.get(pollId);
849
+ if (poll)
850
+ throw new Error(
851
+ `wasi-io trap: Cannot drop ${polledResourceDebugName} as it has a child poll resource which has not yet been dropped`
852
+ );
853
+ }
854
+ }
855
+
856
+ /**
857
+ * @param {PollState} pollState
858
+ * @param {bool} finished
859
+ */
860
+ export function pollStateReady(pollState) {
861
+ if (pollState.ready && pollState.listener) {
862
+ uncaughtException = new Error(
863
+ "wasi-io trap: poll already ready with listener attached"
864
+ );
865
+ }
866
+ pollState.ready = true;
867
+ if (pollState.listener) {
868
+ pollState.listener();
869
+ pollState.listener = null;
870
+ }
871
+ }
872
+
873
+ /**
874
+ * @param {PollState} pollState
875
+ */
876
+ function pollStateCheck(pollState) {
877
+ if (pollState.ready && pollState.parentStream) {
878
+ // stream ONLY applies to readable streams here
879
+ const stream = pollState.parentStream;
880
+ const res = stream.read(0);
881
+ if (res !== null) {
882
+ throw new Error("wasi-io trap: got data for a null read");
883
+ }
884
+ if (
885
+ pollState.ready &&
886
+ stream.readableLength === 0 &&
887
+ !stream.readableEnded &&
888
+ !stream.errored
889
+ ) {
890
+ pollState.ready = false;
891
+ stream.once("readable", () => {
892
+ pollStateReady(pollState);
893
+ });
894
+ }
895
+ }
896
+ }
897
+
898
+ /**
899
+ *
900
+ * @param {Promise<any>} promise
901
+ * @param {PollState | undefined} pollState
902
+ * @returns {number}
903
+ */
904
+ export function createFuture(promise, pollState) {
905
+ const futureId = ++futureCnt;
906
+ if (pollState) {
907
+ pollState.ready = false;
908
+ } else {
909
+ pollState = {
910
+ ready: false,
911
+ listener: null,
912
+ polls: [],
913
+ parent: null,
914
+ };
915
+ }
916
+ const future = { tag: "ok", val: null };
917
+ futures.set(futureId, { future, pollState });
918
+ promise.then(
919
+ (value) => {
920
+ pollStateReady(pollState);
921
+ future.val = value;
922
+ },
923
+ (value) => {
924
+ pollStateReady(pollState);
925
+ future.tag = "err";
926
+ future.val = value;
927
+ }
565
928
  );
566
- return pollId;
929
+ return futureId;
930
+ }
931
+
932
+ /**
933
+ * @param {number} id
934
+ * @returns {undefined | { tag: 'ok', val: any } | { tag: 'err', val: undefined }}
935
+ * @throws {undefined}
936
+ */
937
+ export function futureTakeValue(id) {
938
+ const future = futures.get(id);
939
+ // Not ready = return undefined
940
+ if (!future.pollState.ready) return undefined;
941
+ // Ready but already taken = return { tag: 'err', val: undefined }
942
+ if (!future.future) return { tag: "err", val: undefined };
943
+ const out = { tag: "ok", val: future.future };
944
+ future.future = null;
945
+ return out;
946
+ }
947
+
948
+ export function futureDispose(id, ownsState) {
949
+ const { pollState } = futures.get(id);
950
+ if (ownsState) verifyPollsDroppedForDrop(pollState, "future");
951
+ return void futures.delete(id);
952
+ }
953
+
954
+ let uncaughtException;
955
+ process.on("uncaughtException", (err) => (uncaughtException = err));
956
+
957
+ // eslint-disable-next-line no-unused-vars
958
+ function trace(msg) {
959
+ const tmpErr = new Error(format(msg));
960
+ log(tmpErr.stack);
567
961
  }
568
962
 
569
- runAsWorker(handle);
963
+ const debug = runAsWorker(handle);