@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.
- package/README.md +2 -2
- package/lib/browser/filesystem.js +6 -5
- package/lib/browser/random.js +1 -1
- package/lib/browser/sockets.js +0 -14
- package/lib/io/calls.js +80 -16
- package/lib/io/worker-http.js +164 -67
- package/lib/io/worker-io.js +207 -68
- package/lib/io/worker-socket-tcp.js +285 -0
- package/lib/io/worker-socket-udp.js +576 -0
- package/lib/io/worker-sockets.js +371 -0
- package/lib/io/worker-thread.js +793 -399
- package/lib/nodejs/cli.js +29 -13
- package/lib/nodejs/clocks.js +9 -6
- package/lib/nodejs/filesystem.js +170 -57
- package/lib/nodejs/http.js +662 -531
- package/lib/nodejs/index.js +0 -3
- package/lib/nodejs/sockets.js +571 -11
- package/lib/synckit/index.js +25 -41
- package/package.json +2 -2
- package/types/interfaces/wasi-http-types.d.ts +53 -41
- package/types/interfaces/wasi-sockets-tcp.d.ts +5 -0
- package/lib/common/assert.js +0 -7
- package/lib/nodejs/sockets/socket-common.js +0 -116
- package/lib/nodejs/sockets/socketopts-bindings.js +0 -94
- package/lib/nodejs/sockets/tcp-socket-impl.js +0 -794
- package/lib/nodejs/sockets/udp-socket-impl.js +0 -628
- package/lib/nodejs/sockets/wasi-sockets.js +0 -320
- package/lib/synckit/index.d.ts +0 -71
package/lib/io/worker-io.js
CHANGED
|
@@ -2,8 +2,10 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { createSyncFn } from "../synckit/index.js";
|
|
3
3
|
import {
|
|
4
4
|
CALL_MASK,
|
|
5
|
-
CALL_SHIFT,
|
|
6
5
|
CALL_TYPE_MASK,
|
|
6
|
+
FILE,
|
|
7
|
+
HTTP_SERVER_INCOMING_HANDLER,
|
|
8
|
+
HTTP,
|
|
7
9
|
INPUT_STREAM_BLOCKING_READ,
|
|
8
10
|
INPUT_STREAM_BLOCKING_SKIP,
|
|
9
11
|
INPUT_STREAM_DISPOSE,
|
|
@@ -23,28 +25,62 @@ import {
|
|
|
23
25
|
OUTPUT_STREAM_WRITE,
|
|
24
26
|
POLL_POLL_LIST,
|
|
25
27
|
POLL_POLLABLE_BLOCK,
|
|
28
|
+
POLL_POLLABLE_DISPOSE,
|
|
26
29
|
POLL_POLLABLE_READY,
|
|
30
|
+
SOCKET_TCP,
|
|
31
|
+
STDERR,
|
|
32
|
+
STDIN,
|
|
33
|
+
STDOUT,
|
|
34
|
+
reverseMap,
|
|
27
35
|
} from "./calls.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
const DEBUG = false;
|
|
36
|
+
import { _rawDebug, exit, stderr, stdout, env } from "node:process";
|
|
31
37
|
|
|
32
38
|
const workerPath = fileURLToPath(
|
|
33
39
|
new URL("./worker-thread.js", import.meta.url)
|
|
34
40
|
);
|
|
35
41
|
|
|
42
|
+
const httpIncomingHandlers = new Map();
|
|
43
|
+
export function registerIncomingHttpHandler(id, handler) {
|
|
44
|
+
httpIncomingHandlers.set(id, handler);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const instanceId = Math.round(Math.random() * 1000).toString();
|
|
48
|
+
const DEBUG_DEFAULT = false;
|
|
49
|
+
const DEBUG =
|
|
50
|
+
env.PREVIEW2_SHIM_DEBUG === "0"
|
|
51
|
+
? false
|
|
52
|
+
: env.PREVIEW2_SHIM_DEBUG === "1"
|
|
53
|
+
? true
|
|
54
|
+
: DEBUG_DEFAULT;
|
|
55
|
+
|
|
36
56
|
/**
|
|
37
57
|
* @type {(call: number, id: number | null, payload: any) -> any}
|
|
38
58
|
*/
|
|
39
|
-
export let ioCall = createSyncFn(workerPath)
|
|
59
|
+
export let ioCall = createSyncFn(workerPath, DEBUG, (type, id, payload) => {
|
|
60
|
+
// 'callbacks' from the worker
|
|
61
|
+
// ONLY happens for an http server incoming handler, and NOTHING else (not even sockets, since accept is sync!)
|
|
62
|
+
if (type !== HTTP_SERVER_INCOMING_HANDLER)
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Internal error: only incoming handler callback is permitted"
|
|
65
|
+
);
|
|
66
|
+
const handler = httpIncomingHandlers.get(id);
|
|
67
|
+
if (!handler)
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Internal error: no incoming handler registered for server ${id}`
|
|
70
|
+
);
|
|
71
|
+
handler(payload);
|
|
72
|
+
});
|
|
40
73
|
if (DEBUG) {
|
|
41
74
|
const _ioCall = ioCall;
|
|
42
75
|
ioCall = function ioCall(num, id, payload) {
|
|
76
|
+
if (typeof id !== "number" && id !== null)
|
|
77
|
+
throw new Error("id must be a number or null");
|
|
43
78
|
let ret;
|
|
44
79
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
num &
|
|
80
|
+
_rawDebug(
|
|
81
|
+
instanceId,
|
|
82
|
+
reverseMap[num & CALL_MASK],
|
|
83
|
+
reverseMap[num & CALL_TYPE_MASK],
|
|
48
84
|
id,
|
|
49
85
|
payload
|
|
50
86
|
);
|
|
@@ -54,15 +90,56 @@ if (DEBUG) {
|
|
|
54
90
|
ret = e;
|
|
55
91
|
throw ret;
|
|
56
92
|
} finally {
|
|
57
|
-
|
|
93
|
+
_rawDebug(instanceId, "->", ret);
|
|
58
94
|
}
|
|
59
95
|
};
|
|
60
96
|
}
|
|
61
97
|
|
|
62
98
|
const symbolDispose = Symbol.dispose || Symbol.for("dispose");
|
|
63
99
|
|
|
100
|
+
const finalizationRegistry = new FinalizationRegistry(
|
|
101
|
+
(dispose) => void dispose()
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const dummySymbol = Symbol();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
*
|
|
108
|
+
* @param {any} resource
|
|
109
|
+
* @param {any} parentResource
|
|
110
|
+
* @param {number} id
|
|
111
|
+
* @param {(number) => void} disposeFn
|
|
112
|
+
*/
|
|
113
|
+
export function registerDispose(resource, parentResource, id, disposeFn) {
|
|
114
|
+
// While strictly speaking all components should handle their disposal,
|
|
115
|
+
// this acts as a last-resort to catch all missed drops through the JS GC.
|
|
116
|
+
// Mainly for two cases - (1) components which are long lived, that get shut
|
|
117
|
+
// down and (2) users that interface with low-level WASI APIs directly in JS
|
|
118
|
+
// for various reasons may end up leaning on JS GC inadvertantly.
|
|
119
|
+
function finalizer() {
|
|
120
|
+
// This has no functional purpose other than to pin a strong reference
|
|
121
|
+
// from the child resource's finalizer to the parent resource, to ensure
|
|
122
|
+
// that we can never finalize a parent resource before a child resource.
|
|
123
|
+
// This makes the generational JS GC become piecewise over child resource
|
|
124
|
+
// graphs (generational at each resource hierarchy level at least).
|
|
125
|
+
if (parentResource?.[dummySymbol]) return;
|
|
126
|
+
disposeFn(id);
|
|
127
|
+
}
|
|
128
|
+
finalizationRegistry.register(resource, finalizer, finalizer);
|
|
129
|
+
return finalizer;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function earlyDispose(finalizer) {
|
|
133
|
+
finalizationRegistry.unregister(finalizer);
|
|
134
|
+
finalizer();
|
|
135
|
+
}
|
|
136
|
+
|
|
64
137
|
const _Error = Error;
|
|
65
138
|
const IoError = class Error extends _Error {
|
|
139
|
+
constructor(payload) {
|
|
140
|
+
super(payload);
|
|
141
|
+
this.payload = payload;
|
|
142
|
+
}
|
|
66
143
|
toDebugString() {
|
|
67
144
|
return this.message;
|
|
68
145
|
}
|
|
@@ -72,24 +149,21 @@ function streamIoErrorCall(call, id, payload) {
|
|
|
72
149
|
try {
|
|
73
150
|
return ioCall(call, id, payload);
|
|
74
151
|
} catch (e) {
|
|
75
|
-
if (e.tag ===
|
|
76
|
-
throw e;
|
|
152
|
+
if (e.tag === "closed") throw e;
|
|
77
153
|
if (e.tag === "last-operation-failed") {
|
|
78
|
-
e.val = new IoError(e.val);
|
|
154
|
+
e.val = new IoError(Object.assign(new Error(e.val.message), e.val));
|
|
79
155
|
throw e;
|
|
80
156
|
}
|
|
81
157
|
// any invalid error is a trap
|
|
82
158
|
console.trace(e);
|
|
83
|
-
|
|
159
|
+
exit(1);
|
|
84
160
|
}
|
|
85
161
|
}
|
|
86
162
|
|
|
87
163
|
class InputStream {
|
|
88
164
|
#id;
|
|
89
165
|
#streamType;
|
|
90
|
-
|
|
91
|
-
return this.#id;
|
|
92
|
-
}
|
|
166
|
+
#finalizer;
|
|
93
167
|
read(len) {
|
|
94
168
|
return streamIoErrorCall(
|
|
95
169
|
INPUT_STREAM_READ | this.#streamType,
|
|
@@ -120,24 +194,65 @@ class InputStream {
|
|
|
120
194
|
}
|
|
121
195
|
subscribe() {
|
|
122
196
|
return pollableCreate(
|
|
123
|
-
ioCall(INPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id)
|
|
197
|
+
ioCall(INPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id),
|
|
198
|
+
this
|
|
124
199
|
);
|
|
125
200
|
}
|
|
126
|
-
[symbolDispose]() {
|
|
127
|
-
ioCall(INPUT_STREAM_DISPOSE | this.#streamType, this.#id);
|
|
128
|
-
}
|
|
129
201
|
static _id(stream) {
|
|
130
202
|
return stream.#id;
|
|
131
203
|
}
|
|
132
204
|
/**
|
|
133
|
-
* @param {
|
|
205
|
+
* @param {FILE | SOCKET_TCP | STDIN | HTTP} streamType
|
|
134
206
|
*/
|
|
135
207
|
static _create(streamType, id) {
|
|
136
208
|
const stream = new InputStream();
|
|
137
209
|
stream.#id = id;
|
|
138
210
|
stream.#streamType = streamType;
|
|
211
|
+
let disposeFn;
|
|
212
|
+
switch (streamType) {
|
|
213
|
+
case FILE:
|
|
214
|
+
disposeFn = fileInputStreamDispose;
|
|
215
|
+
break;
|
|
216
|
+
case SOCKET_TCP:
|
|
217
|
+
disposeFn = socketTcpInputStreamDispose;
|
|
218
|
+
break;
|
|
219
|
+
case STDIN:
|
|
220
|
+
disposeFn = stdinInputStreamDispose;
|
|
221
|
+
break;
|
|
222
|
+
case HTTP:
|
|
223
|
+
disposeFn = httpInputStreamDispose;
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
throw new Error(
|
|
227
|
+
"wasi-io trap: Dispose function not created for stream type " +
|
|
228
|
+
reverseMap[streamType]
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
stream.#finalizer = registerDispose(stream, null, id, disposeFn);
|
|
139
232
|
return stream;
|
|
140
233
|
}
|
|
234
|
+
[symbolDispose]() {
|
|
235
|
+
if (this.#finalizer) {
|
|
236
|
+
earlyDispose(this.#finalizer);
|
|
237
|
+
this.#finalizer = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function fileInputStreamDispose(id) {
|
|
243
|
+
ioCall(INPUT_STREAM_DISPOSE | FILE, id, null);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function socketTcpInputStreamDispose(id) {
|
|
247
|
+
ioCall(INPUT_STREAM_DISPOSE | SOCKET_TCP, id, null);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function stdinInputStreamDispose(id) {
|
|
251
|
+
ioCall(INPUT_STREAM_DISPOSE | STDIN, id, null);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function httpInputStreamDispose(id) {
|
|
255
|
+
ioCall(INPUT_STREAM_DISPOSE | HTTP, id, null);
|
|
141
256
|
}
|
|
142
257
|
|
|
143
258
|
export const inputStreamCreate = InputStream._create;
|
|
@@ -149,9 +264,7 @@ delete InputStream._id;
|
|
|
149
264
|
class OutputStream {
|
|
150
265
|
#id;
|
|
151
266
|
#streamType;
|
|
152
|
-
|
|
153
|
-
return this.#id;
|
|
154
|
-
}
|
|
267
|
+
#finalizer;
|
|
155
268
|
checkWrite(len) {
|
|
156
269
|
return streamIoErrorCall(
|
|
157
270
|
OUTPUT_STREAM_CHECK_WRITE | this.#streamType,
|
|
@@ -169,8 +282,7 @@ class OutputStream {
|
|
|
169
282
|
}
|
|
170
283
|
blockingWriteAndFlush(buf) {
|
|
171
284
|
if (this.#streamType <= STDERR) {
|
|
172
|
-
const stream =
|
|
173
|
-
this.#streamType === STDERR ? process.stderr : process.stdout;
|
|
285
|
+
const stream = this.#streamType === STDERR ? stderr : stdout;
|
|
174
286
|
return void stream.write(buf);
|
|
175
287
|
}
|
|
176
288
|
return streamIoErrorCall(
|
|
@@ -206,16 +318,14 @@ class OutputStream {
|
|
|
206
318
|
return streamIoErrorCall(
|
|
207
319
|
OUTPUT_STREAM_SPLICE | this.#streamType,
|
|
208
320
|
this.#id,
|
|
209
|
-
src.#id,
|
|
210
|
-
len
|
|
321
|
+
{ src: src.#id, len }
|
|
211
322
|
);
|
|
212
323
|
}
|
|
213
324
|
blockingSplice(src, len) {
|
|
214
325
|
return streamIoErrorCall(
|
|
215
326
|
OUTPUT_STREAM_BLOCKING_SPLICE | this.#streamType,
|
|
216
327
|
this.#id,
|
|
217
|
-
src
|
|
218
|
-
len
|
|
328
|
+
{ src: inputStreamId(src), len }
|
|
219
329
|
);
|
|
220
330
|
}
|
|
221
331
|
subscribe() {
|
|
@@ -223,9 +333,6 @@ class OutputStream {
|
|
|
223
333
|
ioCall(OUTPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id)
|
|
224
334
|
);
|
|
225
335
|
}
|
|
226
|
-
[symbolDispose]() {
|
|
227
|
-
ioCall(OUTPUT_STREAM_DISPOSE | this.#streamType, this.#id);
|
|
228
|
-
}
|
|
229
336
|
|
|
230
337
|
static _id(outputStream) {
|
|
231
338
|
return outputStream.#id;
|
|
@@ -238,8 +345,54 @@ class OutputStream {
|
|
|
238
345
|
const stream = new OutputStream();
|
|
239
346
|
stream.#id = id;
|
|
240
347
|
stream.#streamType = streamType;
|
|
348
|
+
let disposeFn;
|
|
349
|
+
switch (streamType) {
|
|
350
|
+
case STDOUT:
|
|
351
|
+
disposeFn = stdoutOutputStreamDispose;
|
|
352
|
+
break;
|
|
353
|
+
case STDERR:
|
|
354
|
+
disposeFn = stderrOutputStreamDispose;
|
|
355
|
+
break;
|
|
356
|
+
case SOCKET_TCP:
|
|
357
|
+
disposeFn = socketTcpOutputStreamDispose;
|
|
358
|
+
break;
|
|
359
|
+
case FILE:
|
|
360
|
+
disposeFn = fileOutputStreamDispose;
|
|
361
|
+
break;
|
|
362
|
+
case HTTP:
|
|
363
|
+
return stream;
|
|
364
|
+
default:
|
|
365
|
+
throw new Error(
|
|
366
|
+
"wasi-io trap: Dispose function not created for stream type " +
|
|
367
|
+
reverseMap[streamType]
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
stream.#finalizer = registerDispose(stream, null, id, disposeFn);
|
|
241
371
|
return stream;
|
|
242
372
|
}
|
|
373
|
+
|
|
374
|
+
[symbolDispose]() {
|
|
375
|
+
if (this.#finalizer) {
|
|
376
|
+
earlyDispose(this.#finalizer);
|
|
377
|
+
this.#finalizer = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function stdoutOutputStreamDispose(id) {
|
|
383
|
+
ioCall(OUTPUT_STREAM_DISPOSE | STDOUT, id);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function stderrOutputStreamDispose(id) {
|
|
387
|
+
ioCall(OUTPUT_STREAM_DISPOSE | STDERR, id);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function socketTcpOutputStreamDispose(id) {
|
|
391
|
+
ioCall(OUTPUT_STREAM_DISPOSE | SOCKET_TCP, id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function fileOutputStreamDispose(id) {
|
|
395
|
+
ioCall(OUTPUT_STREAM_DISPOSE | FILE, id);
|
|
243
396
|
}
|
|
244
397
|
|
|
245
398
|
export const outputStreamCreate = OutputStream._create;
|
|
@@ -252,71 +405,57 @@ export const error = { Error: IoError };
|
|
|
252
405
|
|
|
253
406
|
export const streams = { InputStream, OutputStream };
|
|
254
407
|
|
|
408
|
+
function pollableDispose(id) {
|
|
409
|
+
ioCall(POLL_POLLABLE_DISPOSE, id);
|
|
410
|
+
}
|
|
411
|
+
|
|
255
412
|
class Pollable {
|
|
256
413
|
#id;
|
|
257
|
-
#
|
|
258
|
-
get _id() {
|
|
259
|
-
return this.#id;
|
|
260
|
-
}
|
|
414
|
+
#finalizer;
|
|
261
415
|
ready() {
|
|
262
|
-
if (this.#
|
|
263
|
-
|
|
264
|
-
if (ready) this.#ready = true;
|
|
265
|
-
return ready;
|
|
416
|
+
if (this.#id === 0) return true;
|
|
417
|
+
return ioCall(POLL_POLLABLE_READY, this.#id);
|
|
266
418
|
}
|
|
267
419
|
block() {
|
|
268
|
-
if (
|
|
420
|
+
if (this.#id !== 0) {
|
|
269
421
|
ioCall(POLL_POLLABLE_BLOCK, this.#id);
|
|
270
|
-
this.#ready = true;
|
|
271
422
|
}
|
|
272
423
|
}
|
|
273
424
|
static _getId(pollable) {
|
|
274
425
|
return pollable.#id;
|
|
275
426
|
}
|
|
276
|
-
static _create(id) {
|
|
427
|
+
static _create(id, parent) {
|
|
277
428
|
const pollable = new Pollable();
|
|
278
429
|
pollable.#id = id;
|
|
279
|
-
|
|
430
|
+
pollable.#finalizer = registerDispose(
|
|
431
|
+
pollable,
|
|
432
|
+
parent,
|
|
433
|
+
id,
|
|
434
|
+
pollableDispose
|
|
435
|
+
);
|
|
280
436
|
return pollable;
|
|
281
437
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
438
|
+
[symbolDispose]() {
|
|
439
|
+
if (this.#finalizer) {
|
|
440
|
+
earlyDispose(this.#finalizer);
|
|
441
|
+
this.#finalizer = null;
|
|
442
|
+
}
|
|
287
443
|
}
|
|
288
444
|
}
|
|
289
445
|
|
|
290
446
|
export const pollableCreate = Pollable._create;
|
|
291
447
|
delete Pollable._create;
|
|
292
448
|
|
|
293
|
-
const pollableListToIds = Pollable._listToIds;
|
|
294
|
-
delete Pollable._listToIds;
|
|
295
|
-
|
|
296
|
-
const pollableMarkReady = Pollable._markReady;
|
|
297
|
-
delete Pollable._markReady;
|
|
298
|
-
|
|
299
449
|
const pollableGetId = Pollable._getId;
|
|
300
450
|
delete Pollable._getId;
|
|
301
451
|
|
|
302
452
|
export const poll = {
|
|
303
453
|
Pollable,
|
|
304
454
|
poll(list) {
|
|
305
|
-
|
|
306
|
-
return list.filter((pollable) => {
|
|
307
|
-
if (includeList.includes(pollableGetId(pollable))) {
|
|
308
|
-
pollableMarkReady(pollable);
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
return false;
|
|
312
|
-
});
|
|
455
|
+
return ioCall(POLL_POLL_LIST, null, list.map(pollableGetId));
|
|
313
456
|
},
|
|
314
457
|
};
|
|
315
458
|
|
|
316
|
-
export function resolvedPoll() {
|
|
317
|
-
return pollableCreate(0);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
459
|
export function createPoll(call, id, initPayload) {
|
|
321
460
|
return pollableCreate(ioCall(call, id, initPayload));
|
|
322
461
|
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFuture,
|
|
3
|
+
createReadableStream,
|
|
4
|
+
createReadableStreamPollState,
|
|
5
|
+
createWritableStream,
|
|
6
|
+
futureDispose,
|
|
7
|
+
futureTakeValue,
|
|
8
|
+
pollStateReady,
|
|
9
|
+
verifyPollsDroppedForDrop,
|
|
10
|
+
} from "./worker-thread.js";
|
|
11
|
+
const { TCP, constants: TCPConstants } = process.binding("tcp_wrap");
|
|
12
|
+
import {
|
|
13
|
+
convertSocketError,
|
|
14
|
+
convertSocketErrorCode,
|
|
15
|
+
ipSocketAddress,
|
|
16
|
+
isIPv4MappedAddress,
|
|
17
|
+
isMulticastIpAddress,
|
|
18
|
+
isUnicastIpAddress,
|
|
19
|
+
isWildcardAddress,
|
|
20
|
+
noLookup,
|
|
21
|
+
serializeIpAddress,
|
|
22
|
+
SOCKET_STATE_BIND,
|
|
23
|
+
SOCKET_STATE_BOUND,
|
|
24
|
+
SOCKET_STATE_CLOSED,
|
|
25
|
+
SOCKET_STATE_CONNECT,
|
|
26
|
+
SOCKET_STATE_CONNECTION,
|
|
27
|
+
SOCKET_STATE_INIT,
|
|
28
|
+
SOCKET_STATE_LISTEN,
|
|
29
|
+
SOCKET_STATE_LISTENER,
|
|
30
|
+
} from "./worker-sockets.js";
|
|
31
|
+
import { Socket, Server } from "node:net";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {import("../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress
|
|
35
|
+
* @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily
|
|
36
|
+
* @typedef {import("node:net").Socket} TcpSocket
|
|
37
|
+
*
|
|
38
|
+
* @typedef {{
|
|
39
|
+
* tcpSocket: number,
|
|
40
|
+
* err: Error | null,
|
|
41
|
+
* pollState: PollState,
|
|
42
|
+
* }} PendingAccept
|
|
43
|
+
*
|
|
44
|
+
* @typedef {{
|
|
45
|
+
* state: number,
|
|
46
|
+
* future: number | null,
|
|
47
|
+
* socket: TcpSocket | null,
|
|
48
|
+
* listenBacklogSize: number,
|
|
49
|
+
* handle: TCP,
|
|
50
|
+
* pendingAccepts: PendingAccept[],
|
|
51
|
+
* pollState: PollState,
|
|
52
|
+
* }} TcpSocketRecord
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @type {Map<number, TcpSocketRecord>}
|
|
57
|
+
*/
|
|
58
|
+
export const tcpSockets = new Map();
|
|
59
|
+
|
|
60
|
+
let tcpSocketCnt = 0;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {IpAddressFamily} addressFamily
|
|
64
|
+
*/
|
|
65
|
+
export function createTcpSocket() {
|
|
66
|
+
const handle = new TCP(TCPConstants.SOCKET);
|
|
67
|
+
tcpSockets.set(++tcpSocketCnt, {
|
|
68
|
+
state: SOCKET_STATE_INIT,
|
|
69
|
+
future: null,
|
|
70
|
+
listenBacklogSize: 128,
|
|
71
|
+
handle,
|
|
72
|
+
pendingAccepts: [],
|
|
73
|
+
pollState: { ready: true, listener: null, polls: [], parentStream: null },
|
|
74
|
+
});
|
|
75
|
+
return tcpSocketCnt;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function socketTcpFinish(id, fromState, toState) {
|
|
79
|
+
const socket = tcpSockets.get(id);
|
|
80
|
+
if (socket.state !== fromState) throw "not-in-progress";
|
|
81
|
+
if (!socket.pollState.ready) throw "would-block";
|
|
82
|
+
const { tag, val } = futureTakeValue(socket.future).val;
|
|
83
|
+
futureDispose(socket.future, false);
|
|
84
|
+
socket.future = null;
|
|
85
|
+
if (tag === "err") {
|
|
86
|
+
socket.state = SOCKET_STATE_CLOSED;
|
|
87
|
+
throw val;
|
|
88
|
+
} else {
|
|
89
|
+
socket.state = toState;
|
|
90
|
+
// for the listener, we must immediately transition back to unresolved
|
|
91
|
+
if (toState === SOCKET_STATE_LISTENER) socket.pollState.ready = false;
|
|
92
|
+
return val;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function socketTcpBindStart(id, localAddress, family) {
|
|
97
|
+
const socket = tcpSockets.get(id);
|
|
98
|
+
if (socket.state !== SOCKET_STATE_INIT) throw "invalid-state";
|
|
99
|
+
if (
|
|
100
|
+
family !== localAddress.tag ||
|
|
101
|
+
!isUnicastIpAddress(localAddress) ||
|
|
102
|
+
isIPv4MappedAddress(localAddress)
|
|
103
|
+
)
|
|
104
|
+
throw "invalid-argument";
|
|
105
|
+
socket.state = SOCKET_STATE_BIND;
|
|
106
|
+
const { handle } = socket;
|
|
107
|
+
socket.future = createFuture(
|
|
108
|
+
(async () => {
|
|
109
|
+
const address = serializeIpAddress(localAddress);
|
|
110
|
+
const port = localAddress.val.port;
|
|
111
|
+
const code =
|
|
112
|
+
localAddress.tag === "ipv6"
|
|
113
|
+
? handle.bind6(address, port, TCPConstants.UV_TCP_IPV6ONLY)
|
|
114
|
+
: handle.bind(address, port);
|
|
115
|
+
if (code !== 0) throw convertSocketErrorCode(-code);
|
|
116
|
+
// This is a Node.js / libuv quirk to force the bind error to be thrown
|
|
117
|
+
// (specifically address-in-use).
|
|
118
|
+
{
|
|
119
|
+
const out = {};
|
|
120
|
+
const code = handle.getsockname(out);
|
|
121
|
+
if (code !== 0) throw convertSocketErrorCode(-code);
|
|
122
|
+
}
|
|
123
|
+
})(),
|
|
124
|
+
socket.pollState
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function socketTcpConnectStart(id, remoteAddress, family) {
|
|
129
|
+
const socket = tcpSockets.get(id);
|
|
130
|
+
if (socket.state !== SOCKET_STATE_INIT && socket.state !== SOCKET_STATE_BOUND)
|
|
131
|
+
throw "invalid-state";
|
|
132
|
+
if (
|
|
133
|
+
isWildcardAddress(remoteAddress) ||
|
|
134
|
+
family !== remoteAddress.tag ||
|
|
135
|
+
!isUnicastIpAddress(remoteAddress) ||
|
|
136
|
+
isMulticastIpAddress(remoteAddress) ||
|
|
137
|
+
remoteAddress.val.port === 0 ||
|
|
138
|
+
isIPv4MappedAddress(remoteAddress)
|
|
139
|
+
) {
|
|
140
|
+
throw "invalid-argument";
|
|
141
|
+
}
|
|
142
|
+
socket.state = SOCKET_STATE_CONNECT;
|
|
143
|
+
socket.future = createFuture(
|
|
144
|
+
new Promise((resolve, reject) => {
|
|
145
|
+
const tcpSocket = (socket.tcpSocket = new Socket({
|
|
146
|
+
handle: socket.handle,
|
|
147
|
+
pauseOnCreate: true,
|
|
148
|
+
allowHalfOpen: true,
|
|
149
|
+
}));
|
|
150
|
+
function handleErr(err) {
|
|
151
|
+
tcpSocket.off("connect", handleConnect);
|
|
152
|
+
reject(convertSocketError(err));
|
|
153
|
+
}
|
|
154
|
+
function handleConnect() {
|
|
155
|
+
tcpSocket.off("error", handleErr);
|
|
156
|
+
resolve([
|
|
157
|
+
createReadableStream(tcpSocket),
|
|
158
|
+
createWritableStream(tcpSocket),
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
tcpSocket.once("connect", handleConnect);
|
|
162
|
+
tcpSocket.once("error", handleErr);
|
|
163
|
+
tcpSocket.connect({
|
|
164
|
+
port: remoteAddress.val.port,
|
|
165
|
+
host: serializeIpAddress(remoteAddress),
|
|
166
|
+
lookup: noLookup,
|
|
167
|
+
});
|
|
168
|
+
}),
|
|
169
|
+
socket.pollState
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function socketTcpListenStart(id) {
|
|
174
|
+
const socket = tcpSockets.get(id);
|
|
175
|
+
if (socket.state !== SOCKET_STATE_BOUND) throw "invalid-state";
|
|
176
|
+
const { handle } = socket;
|
|
177
|
+
socket.state = SOCKET_STATE_LISTEN;
|
|
178
|
+
socket.future = createFuture(
|
|
179
|
+
new Promise((resolve, reject) => {
|
|
180
|
+
const server = new Server({ pauseOnConnect: true, allowHalfOpen: true });
|
|
181
|
+
function handleErr(err) {
|
|
182
|
+
server.off("listening", handleListen);
|
|
183
|
+
reject(convertSocketError(err));
|
|
184
|
+
}
|
|
185
|
+
function handleListen() {
|
|
186
|
+
server.off("error", handleErr);
|
|
187
|
+
server.on("connection", (tcpSocket) => {
|
|
188
|
+
pollStateReady(socket.pollState);
|
|
189
|
+
const pollState = createReadableStreamPollState(tcpSocket);
|
|
190
|
+
socket.pendingAccepts.push({ tcpSocket, err: null, pollState });
|
|
191
|
+
});
|
|
192
|
+
server.on("error", (err) => {
|
|
193
|
+
pollStateReady(socket.pollState);
|
|
194
|
+
socket.pendingAccepts.push({ tcpSocket: null, err, pollState: null });
|
|
195
|
+
});
|
|
196
|
+
resolve();
|
|
197
|
+
}
|
|
198
|
+
server.once("listening", handleListen);
|
|
199
|
+
server.once("error", handleErr);
|
|
200
|
+
server.listen(handle, socket.listenBacklogSize);
|
|
201
|
+
}),
|
|
202
|
+
socket.pollState
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function socketTcpAccept(id) {
|
|
207
|
+
const socket = tcpSockets.get(id);
|
|
208
|
+
if (socket.state !== SOCKET_STATE_LISTENER) throw "invalid-state";
|
|
209
|
+
if (socket.pendingAccepts.length === 0) throw "would-block";
|
|
210
|
+
const accept = socket.pendingAccepts.shift();
|
|
211
|
+
if (accept.err) {
|
|
212
|
+
socket.state = SOCKET_STATE_CLOSED;
|
|
213
|
+
throw convertSocketError(accept.err);
|
|
214
|
+
}
|
|
215
|
+
if (socket.pendingAccepts.length === 0) socket.pollState.ready = false;
|
|
216
|
+
tcpSockets.set(++tcpSocketCnt, {
|
|
217
|
+
state: SOCKET_STATE_CONNECTION,
|
|
218
|
+
future: null,
|
|
219
|
+
listenBacklogSize: 128,
|
|
220
|
+
handle: accept.tcpSocket._handle,
|
|
221
|
+
pendingAccepts: [],
|
|
222
|
+
pollState: accept.pollState,
|
|
223
|
+
});
|
|
224
|
+
return [
|
|
225
|
+
tcpSocketCnt,
|
|
226
|
+
createReadableStream(accept.tcpSocket, accept.pollState),
|
|
227
|
+
createWritableStream(accept.tcpSocket),
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function socketTcpSetListenBacklogSize(id, backlogSize) {
|
|
232
|
+
const socket = tcpSockets.get(id);
|
|
233
|
+
if (
|
|
234
|
+
socket.state === SOCKET_STATE_LISTEN ||
|
|
235
|
+
socket.state === SOCKET_STATE_LISTENER
|
|
236
|
+
)
|
|
237
|
+
throw "not-supported";
|
|
238
|
+
if (
|
|
239
|
+
socket.state !== SOCKET_STATE_INIT &&
|
|
240
|
+
socket.state !== SOCKET_STATE_BIND &&
|
|
241
|
+
socket.state !== SOCKET_STATE_BOUND
|
|
242
|
+
)
|
|
243
|
+
throw "invalid-state";
|
|
244
|
+
socket.listenBacklogSize = Number(backlogSize);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function socketTcpGetLocalAddress(id) {
|
|
248
|
+
const { handle } = tcpSockets.get(id);
|
|
249
|
+
const out = {};
|
|
250
|
+
const code = handle.getsockname(out);
|
|
251
|
+
if (code !== 0) throw convertSocketErrorCode(-code);
|
|
252
|
+
return ipSocketAddress(out.family.toLowerCase(), out.address, out.port);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function socketTcpGetRemoteAddress(id) {
|
|
256
|
+
const { handle } = tcpSockets.get(id);
|
|
257
|
+
const out = {};
|
|
258
|
+
const code = handle.getpeername(out);
|
|
259
|
+
if (code !== 0) throw convertSocketErrorCode(-code);
|
|
260
|
+
return ipSocketAddress(out.family.toLowerCase(), out.address, out.port);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function socketTcpShutdown(id, shutdownType) {
|
|
264
|
+
const socket = tcpSockets.get(id);
|
|
265
|
+
if (socket.state !== SOCKET_STATE_CONNECTION) throw "invalid-state";
|
|
266
|
+
// Node.js only supports a write shutdown, which is triggered on end
|
|
267
|
+
if (shutdownType === "send" || shutdownType === "both")
|
|
268
|
+
socket.tcpSocket.end();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function socketTcpSetKeepAlive(id, { keepAlive, keepAliveIdleTime }) {
|
|
272
|
+
const { handle } = tcpSockets.get(id);
|
|
273
|
+
const code = handle.setKeepAlive(
|
|
274
|
+
keepAlive,
|
|
275
|
+
Number(keepAliveIdleTime / 1_000_000_000n)
|
|
276
|
+
);
|
|
277
|
+
if (code !== 0) throw convertSocketErrorCode(-code);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function socketTcpDispose(id) {
|
|
281
|
+
const socket = tcpSockets.get(id);
|
|
282
|
+
verifyPollsDroppedForDrop(socket.pollState, "tcp socket");
|
|
283
|
+
socket.handle.close();
|
|
284
|
+
tcpSockets.delete(id);
|
|
285
|
+
}
|