@bytecodealliance/preview3-shim 0.1.0-rc.0 → 0.1.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/lib/nodejs/future.js +3 -0
- package/lib/nodejs/http/client.js +16 -1
- package/lib/nodejs/http/request.js +94 -3
- package/lib/nodejs/http/response.js +20 -5
- package/lib/nodejs/sockets/tcp.js +22 -1
- package/lib/nodejs/stream.js +18 -4
- package/lib/nodejs/workers/http-worker.js +33 -3
- package/lib/nodejs/workers/resource-worker.js +47 -3
- package/lib/nodejs/workers/tcp-worker.js +104 -17
- package/package.json +1 -1
package/lib/nodejs/future.js
CHANGED
|
@@ -136,7 +136,7 @@ const responseFromParts = (parts) => {
|
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
const future = new
|
|
139
|
+
const future = new TrailerFutureReader(promise.then(_trailerResultFromEntries), trailers);
|
|
140
140
|
const contents = new StreamReader(body);
|
|
141
141
|
const fields = _fieldsFromEntriesChecked(headers);
|
|
142
142
|
|
|
@@ -144,3 +144,18 @@ const responseFromParts = (parts) => {
|
|
|
144
144
|
res.setStatusCode(statusCode);
|
|
145
145
|
return res;
|
|
146
146
|
};
|
|
147
|
+
|
|
148
|
+
class TrailerFutureReader extends FutureReader {
|
|
149
|
+
#trailers;
|
|
150
|
+
|
|
151
|
+
constructor(promise, trailers) {
|
|
152
|
+
super(promise);
|
|
153
|
+
this.#trailers = trailers;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
close() {
|
|
157
|
+
this.#trailers?.close();
|
|
158
|
+
this.#trailers = null;
|
|
159
|
+
super.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -2,7 +2,12 @@ import { HttpError } from "./error.js";
|
|
|
2
2
|
import { _fieldsLock, Fields } from "./fields.js";
|
|
3
3
|
|
|
4
4
|
import { FutureReader, future } from "../future.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_BYTE_STREAM_CHUNK_SIZE,
|
|
7
|
+
StreamReader,
|
|
8
|
+
_byteChunk,
|
|
9
|
+
readableByteStreamFromReader,
|
|
10
|
+
} from "../stream.js";
|
|
6
11
|
|
|
7
12
|
const symbolDispose = Symbol.dispose || Symbol.for("dispose");
|
|
8
13
|
|
|
@@ -221,7 +226,7 @@ export class Request {
|
|
|
221
226
|
if (contents != null && !(contents instanceof StreamReader)) {
|
|
222
227
|
try {
|
|
223
228
|
dispose = contents[symbolDispose]?.bind(contents);
|
|
224
|
-
const inner =
|
|
229
|
+
const inner = readableContentsStream(contents, headers);
|
|
225
230
|
contents = new StreamReader(inner);
|
|
226
231
|
} catch (err) {
|
|
227
232
|
throw new HttpError("invalid-argument", err.message);
|
|
@@ -231,7 +236,6 @@ export class Request {
|
|
|
231
236
|
// Generated P3 futures are lazy thenables so we want to observe them now so early error paths don't hang.
|
|
232
237
|
if (!(trailers instanceof FutureReader)) {
|
|
233
238
|
const promise = Promise.resolve(trailers);
|
|
234
|
-
void promise.catch(() => {});
|
|
235
239
|
trailers = new FutureReader(promise);
|
|
236
240
|
}
|
|
237
241
|
|
|
@@ -477,6 +481,93 @@ export class Request {
|
|
|
477
481
|
}
|
|
478
482
|
}
|
|
479
483
|
|
|
484
|
+
function readableContentsStream(reader, headers) {
|
|
485
|
+
const expectedLength = contentLength(headers.copyAll());
|
|
486
|
+
if (expectedLength === null) {
|
|
487
|
+
return readableByteStreamFromReader(reader, { name: "contents" });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const source =
|
|
491
|
+
typeof reader?.read === "function"
|
|
492
|
+
? reader
|
|
493
|
+
: new StreamReader(readableByteStreamFromReader(reader, { name: "contents" }));
|
|
494
|
+
return readableByteStreamFromReader(contentLengthReader(source, expectedLength), {
|
|
495
|
+
name: "contents",
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function contentLengthReader(reader, expectedLength) {
|
|
500
|
+
let sent = 0n;
|
|
501
|
+
return {
|
|
502
|
+
async read() {
|
|
503
|
+
const result = await reader.read(readOpts(expectedLength - sent));
|
|
504
|
+
if (isIteratorResult(result) && result.rejectedLength !== undefined) {
|
|
505
|
+
throw requestBodySizeError(sent + BigInt(result.rejectedLength));
|
|
506
|
+
}
|
|
507
|
+
if (isIteratorResult(result) && result.done) {
|
|
508
|
+
if (sent < expectedLength) {
|
|
509
|
+
throw requestBodySizeError(sent);
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const value = isIteratorResult(result) ? result.value : result;
|
|
515
|
+
const chunk = _byteChunk(value);
|
|
516
|
+
sent += BigInt(chunk.byteLength);
|
|
517
|
+
if (sent > expectedLength) {
|
|
518
|
+
throw requestBodySizeError(sent);
|
|
519
|
+
}
|
|
520
|
+
return { value: chunk, done: false };
|
|
521
|
+
},
|
|
522
|
+
cancel(reason) {
|
|
523
|
+
if (typeof reader.cancel === "function") {
|
|
524
|
+
return reader.cancel(reason);
|
|
525
|
+
}
|
|
526
|
+
if (typeof reader.close === "function") {
|
|
527
|
+
return reader.close();
|
|
528
|
+
}
|
|
529
|
+
return reader[symbolDispose]?.();
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function readOpts(remaining) {
|
|
535
|
+
if (remaining <= 0n) {
|
|
536
|
+
return { count: 0, rejectLength: 0 };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const count = Number(
|
|
540
|
+
remaining <= BigInt(DEFAULT_BYTE_STREAM_CHUNK_SIZE)
|
|
541
|
+
? remaining
|
|
542
|
+
: BigInt(DEFAULT_BYTE_STREAM_CHUNK_SIZE),
|
|
543
|
+
);
|
|
544
|
+
const opts = { count };
|
|
545
|
+
if (remaining <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
546
|
+
opts.rejectLength = Number(remaining);
|
|
547
|
+
}
|
|
548
|
+
return opts;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function requestBodySizeError(bytes) {
|
|
552
|
+
return { tag: "HTTP-request-body-size", val: bytes };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isIteratorResult(value) {
|
|
556
|
+
return value != null && typeof value === "object" && typeof value.done === "boolean";
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const decoder = new TextDecoder();
|
|
560
|
+
|
|
561
|
+
const contentLength = (entries) => {
|
|
562
|
+
const entry = entries.findLast(([name]) => name.toLowerCase() === "content-length");
|
|
563
|
+
if (!entry) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const value = decoder.decode(entry[1]);
|
|
568
|
+
return /^\d+$/.test(value) ? BigInt(value) : null;
|
|
569
|
+
};
|
|
570
|
+
|
|
480
571
|
const UrlPart = {
|
|
481
572
|
PATH_WITH_QUERY: "pathWithQuery",
|
|
482
573
|
SCHEME: "scheme",
|
|
@@ -71,7 +71,6 @@ export class Response {
|
|
|
71
71
|
// Generated P3 futures are lazy thenables so we want to observe them now so early error paths don't hang.
|
|
72
72
|
if (!(trailers instanceof FutureReader)) {
|
|
73
73
|
const promise = Promise.resolve(trailers);
|
|
74
|
-
void promise.catch(() => {});
|
|
75
74
|
trailers = new FutureReader(promise);
|
|
76
75
|
}
|
|
77
76
|
|
|
@@ -90,7 +89,7 @@ export class Response {
|
|
|
90
89
|
[symbolDispose]() {
|
|
91
90
|
if (this.#contents && !this.#bodyOpen && !this.#bodyEnded) {
|
|
92
91
|
this.#contentsDispose?.();
|
|
93
|
-
this.#
|
|
92
|
+
this.#closeBody();
|
|
94
93
|
}
|
|
95
94
|
this.#contents = null;
|
|
96
95
|
this.#contentsDispose = null;
|
|
@@ -194,9 +193,12 @@ export class Response {
|
|
|
194
193
|
|
|
195
194
|
const closedFn = reader.close.bind(reader);
|
|
196
195
|
reader.close = () => {
|
|
197
|
-
closedFn
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
response.#closeBody(closedFn);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const cancelFn = reader.cancel.bind(reader);
|
|
200
|
+
reader.cancel = async (...args) => {
|
|
201
|
+
await response.#closeBody(() => cancelFn(...args));
|
|
200
202
|
};
|
|
201
203
|
|
|
202
204
|
return [response.#contents, response.#trailersFuture];
|
|
@@ -209,4 +211,17 @@ export class Response {
|
|
|
209
211
|
this.#responseFuture = null;
|
|
210
212
|
}
|
|
211
213
|
}
|
|
214
|
+
|
|
215
|
+
#closeBody(close = this.#contents?.close.bind(this.#contents)) {
|
|
216
|
+
const closeTrailers = !this.#bodyEnded;
|
|
217
|
+
try {
|
|
218
|
+
return close?.();
|
|
219
|
+
} finally {
|
|
220
|
+
if (closeTrailers) {
|
|
221
|
+
this.#trailersFuture.close();
|
|
222
|
+
}
|
|
223
|
+
this.#bodyEnded = true;
|
|
224
|
+
this.#bodyOpen = false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
212
227
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./address.js";
|
|
14
14
|
|
|
15
15
|
let WORKER = null;
|
|
16
|
+
const symbolDispose = Symbol.dispose || Symbol.for("dispose");
|
|
16
17
|
function worker() {
|
|
17
18
|
return (WORKER ??= new ResourceWorker(new URL("../workers/tcp-worker.js", import.meta.url)));
|
|
18
19
|
}
|
|
@@ -61,6 +62,8 @@ export class TcpSocket {
|
|
|
61
62
|
#socketId = null;
|
|
62
63
|
#family = null;
|
|
63
64
|
#state = "unbound";
|
|
65
|
+
#sendStarted = false;
|
|
66
|
+
#receiveStarted = false;
|
|
64
67
|
#options = {
|
|
65
68
|
// defaults per https://nodejs.org/docs/latest/api/net.html#socketsetkeepaliveenable-initialdelay
|
|
66
69
|
keepAliveEnabled: false,
|
|
@@ -270,6 +273,10 @@ export class TcpSocket {
|
|
|
270
273
|
if (this.#state !== STATE.CONNECTED) {
|
|
271
274
|
throw new SocketError("invalid-state");
|
|
272
275
|
}
|
|
276
|
+
if (this.#sendStarted) {
|
|
277
|
+
data?.[symbolDispose]?.();
|
|
278
|
+
throw new SocketError("invalid-state");
|
|
279
|
+
}
|
|
273
280
|
let stream;
|
|
274
281
|
try {
|
|
275
282
|
stream = readableByteStreamFromReader(data, { name: "tcp send data" });
|
|
@@ -279,6 +286,7 @@ export class TcpSocket {
|
|
|
279
286
|
}
|
|
280
287
|
throw error;
|
|
281
288
|
}
|
|
289
|
+
this.#sendStarted = true;
|
|
282
290
|
|
|
283
291
|
try {
|
|
284
292
|
// Transfer the stream to the worker
|
|
@@ -310,6 +318,16 @@ export class TcpSocket {
|
|
|
310
318
|
if (this.#state !== STATE.CONNECTED) {
|
|
311
319
|
throw new SocketError("invalid-state");
|
|
312
320
|
}
|
|
321
|
+
if (this.#receiveStarted) {
|
|
322
|
+
const stream = new ReadableStream({
|
|
323
|
+
start(controller) {
|
|
324
|
+
controller.close();
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
const promise = Promise.reject(new SocketError("invalid-state"));
|
|
328
|
+
return [new StreamReader(stream, { preventCancel: false }), new FutureReader(promise)];
|
|
329
|
+
}
|
|
330
|
+
this.#receiveStarted = true;
|
|
313
331
|
|
|
314
332
|
const transform = new TransformStream();
|
|
315
333
|
const promise = worker()
|
|
@@ -325,7 +343,10 @@ export class TcpSocket {
|
|
|
325
343
|
throw SocketError.from(err);
|
|
326
344
|
});
|
|
327
345
|
|
|
328
|
-
return [
|
|
346
|
+
return [
|
|
347
|
+
new StreamReader(transform.readable, { preventCancel: false }),
|
|
348
|
+
new FutureReader(promise),
|
|
349
|
+
];
|
|
329
350
|
}
|
|
330
351
|
|
|
331
352
|
/**
|
package/lib/nodejs/stream.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const DEFAULT_BYTE_STREAM_CHUNK_SIZE = 64 * 1024;
|
|
2
|
+
const symbolDispose = Symbol.dispose || Symbol.for("dispose");
|
|
2
3
|
|
|
3
4
|
let BYTE_STREAM_ENCODER = null;
|
|
4
5
|
function encoder() {
|
|
@@ -61,7 +62,7 @@ export function readableByteStreamFromReader(reader, opts = {}) {
|
|
|
61
62
|
controller.close();
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
|
-
controller.enqueue(
|
|
65
|
+
controller.enqueue(_byteChunk(value));
|
|
65
66
|
},
|
|
66
67
|
cancel(reason) {
|
|
67
68
|
return source.cancel?.(reason);
|
|
@@ -85,6 +86,15 @@ function byteStreamSource(reader, opts) {
|
|
|
85
86
|
}
|
|
86
87
|
return { value: result, done: result === null };
|
|
87
88
|
},
|
|
89
|
+
cancel(reason) {
|
|
90
|
+
if (typeof reader.cancel === "function") {
|
|
91
|
+
return reader.cancel(reason);
|
|
92
|
+
}
|
|
93
|
+
if (typeof reader.close === "function") {
|
|
94
|
+
return reader.close();
|
|
95
|
+
}
|
|
96
|
+
return reader[symbolDispose]?.();
|
|
97
|
+
},
|
|
88
98
|
};
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -107,7 +117,7 @@ function isIteratorResult(value) {
|
|
|
107
117
|
return value != null && typeof value === "object" && typeof value.done === "boolean";
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
function
|
|
120
|
+
export function _byteChunk(value) {
|
|
111
121
|
if (value instanceof Uint8Array) {
|
|
112
122
|
return value;
|
|
113
123
|
}
|
|
@@ -196,7 +206,7 @@ export class StreamReader {
|
|
|
196
206
|
* @param {AsyncIterable|Iterable} source - An async or sync iterable to consume e.g. ReadableStream, async generator, array.
|
|
197
207
|
* @throws {Error} If the provided source does not implement `[Symbol.asyncIterator]` or `[Symbol.iterator]`.
|
|
198
208
|
*/
|
|
199
|
-
constructor(source) {
|
|
209
|
+
constructor(source, opts = {}) {
|
|
200
210
|
if (
|
|
201
211
|
!source ||
|
|
202
212
|
(typeof source[Symbol.asyncIterator] !== "function" &&
|
|
@@ -210,7 +220,7 @@ export class StreamReader {
|
|
|
210
220
|
// For ReadableStream, use values() with preventCancel so the underlying
|
|
211
221
|
// stream is not cancelled when the iterator is released.
|
|
212
222
|
if (source instanceof ReadableStream) {
|
|
213
|
-
this.#iterator = source.values({ preventCancel: true });
|
|
223
|
+
this.#iterator = source.values({ preventCancel: opts.preventCancel ?? true });
|
|
214
224
|
} else if (typeof source[Symbol.asyncIterator] === "function") {
|
|
215
225
|
this.#iterator = source[Symbol.asyncIterator]();
|
|
216
226
|
} else {
|
|
@@ -298,6 +308,10 @@ export class StreamReader {
|
|
|
298
308
|
this.#iterator = null;
|
|
299
309
|
}
|
|
300
310
|
|
|
311
|
+
[symbolDispose]() {
|
|
312
|
+
this.close();
|
|
313
|
+
}
|
|
314
|
+
|
|
301
315
|
/**
|
|
302
316
|
* Consumes the reader and returns the underlying async iterator.
|
|
303
317
|
*
|
|
@@ -222,7 +222,10 @@ async function doHandleRequest({ url, method, headers, trailers, body, timeouts
|
|
|
222
222
|
req.once("close", onClose);
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
const
|
|
225
|
+
const expectedLength = contentLength(headers);
|
|
226
|
+
const upload = body
|
|
227
|
+
? sendRequestBody(req, body, trailers, () => resStarted, expectedLength)
|
|
228
|
+
: endRequest(req);
|
|
226
229
|
|
|
227
230
|
upload.then(
|
|
228
231
|
() => transmit.ok(),
|
|
@@ -287,10 +290,12 @@ async function handleHttpServerClose({ serverId }) {
|
|
|
287
290
|
return serverId;
|
|
288
291
|
}
|
|
289
292
|
|
|
290
|
-
async function sendRequestBody(req, body, trailers, resStarted) {
|
|
293
|
+
async function sendRequestBody(req, body, trailers, resStarted, expectedLength) {
|
|
291
294
|
try {
|
|
292
295
|
req.flushHeaders();
|
|
293
|
-
await pipeline(Readable.fromWeb(body), req, {
|
|
296
|
+
await pipeline(validateRequestBody(Readable.fromWeb(body), expectedLength), req, {
|
|
297
|
+
end: false,
|
|
298
|
+
});
|
|
294
299
|
const fields = await recvTrailers(trailers);
|
|
295
300
|
if (fields) {
|
|
296
301
|
req.addTrailers(toObject(fields));
|
|
@@ -304,6 +309,21 @@ async function sendRequestBody(req, body, trailers, resStarted) {
|
|
|
304
309
|
}
|
|
305
310
|
}
|
|
306
311
|
|
|
312
|
+
async function* validateRequestBody(body, expectedLength) {
|
|
313
|
+
let bytes = 0n;
|
|
314
|
+
for await (const chunk of body) {
|
|
315
|
+
bytes += BigInt(chunk.byteLength);
|
|
316
|
+
if (expectedLength !== null && bytes > expectedLength) {
|
|
317
|
+
throw new HttpError("HTTP-request-body-size", undefined, bytes);
|
|
318
|
+
}
|
|
319
|
+
yield chunk;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (expectedLength !== null && bytes < expectedLength) {
|
|
323
|
+
throw new HttpError("HTTP-request-body-size", undefined, bytes);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
307
327
|
function endRequest(req) {
|
|
308
328
|
return new Promise((resolve, reject) => {
|
|
309
329
|
let settled = false;
|
|
@@ -374,6 +394,16 @@ const msecs = (time) => {
|
|
|
374
394
|
|
|
375
395
|
const decoder = new TextDecoder();
|
|
376
396
|
|
|
397
|
+
const contentLength = (entries) => {
|
|
398
|
+
const entry = entries.findLast(([name]) => name.toLowerCase() === "content-length");
|
|
399
|
+
if (!entry) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const value = decoder.decode(entry[1]);
|
|
404
|
+
return /^\d+$/.test(value) ? BigInt(value) : null;
|
|
405
|
+
};
|
|
406
|
+
|
|
377
407
|
const toObject = (entries) => {
|
|
378
408
|
return Object.fromEntries(entries.map(([key, val]) => [key, decoder.decode(val)]));
|
|
379
409
|
};
|
|
@@ -46,7 +46,7 @@ export class ResourceWorker {
|
|
|
46
46
|
port1.close();
|
|
47
47
|
|
|
48
48
|
if (error) {
|
|
49
|
-
reject(error);
|
|
49
|
+
reject(deserializeError(error));
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
resolve(result);
|
|
@@ -77,7 +77,7 @@ export class ResourceWorker {
|
|
|
77
77
|
const { result, error } = message;
|
|
78
78
|
|
|
79
79
|
if (error) {
|
|
80
|
-
throw error;
|
|
80
|
+
throw deserializeError(error);
|
|
81
81
|
}
|
|
82
82
|
return result;
|
|
83
83
|
}
|
|
@@ -118,7 +118,7 @@ export function Router() {
|
|
|
118
118
|
result = outcome;
|
|
119
119
|
}
|
|
120
120
|
} catch (err) {
|
|
121
|
-
error = err;
|
|
121
|
+
error = serializeError(err);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
_reply.postMessage({ result, error }, transfer);
|
|
@@ -144,6 +144,50 @@ export function Router() {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function serializeError(err) {
|
|
148
|
+
if (!(err instanceof Error)) {
|
|
149
|
+
return err;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const serialized = {
|
|
153
|
+
__resourceWorkerError: true,
|
|
154
|
+
name: err.name,
|
|
155
|
+
message: err.message,
|
|
156
|
+
stack: err.stack,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
for (const key of Reflect.ownKeys(err)) {
|
|
160
|
+
serialized[key] = err[key];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (err.cause !== undefined) {
|
|
164
|
+
serialized.cause = serializeError(err.cause);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return serialized;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function deserializeError(err) {
|
|
171
|
+
if (!err || typeof err !== "object" || err.__resourceWorkerError !== true) {
|
|
172
|
+
return err;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const error = new Error(err.message);
|
|
176
|
+
error.name = err.name;
|
|
177
|
+
if (err.stack) {
|
|
178
|
+
error.stack = err.stack;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const key of Reflect.ownKeys(err)) {
|
|
182
|
+
if (key === "__resourceWorkerError" || key === "name" || key === "message" || key === "stack") {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
error[key] = key === "cause" ? deserializeError(err[key]) : err[key];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return error;
|
|
189
|
+
}
|
|
190
|
+
|
|
147
191
|
function notify(condvar) {
|
|
148
192
|
const lock = new Int32Array(condvar);
|
|
149
193
|
Atomics.store(lock, 0, 1);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Socket, Server } from "node:net";
|
|
2
|
-
import { Readable
|
|
2
|
+
import { Readable } from "stream";
|
|
3
3
|
import { pipeline } from "stream/promises";
|
|
4
4
|
import { once } from "node:events";
|
|
5
5
|
|
|
@@ -48,6 +48,8 @@ function handleTcpCreate({ family }) {
|
|
|
48
48
|
server: null,
|
|
49
49
|
backlog: 128,
|
|
50
50
|
localAddress: null,
|
|
51
|
+
disposed: false,
|
|
52
|
+
activeStreams: 0,
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
return { socketId };
|
|
@@ -143,6 +145,7 @@ async function handleTcpListen({ socketId, stream }) {
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
server.on("connection", (conn) => {
|
|
148
|
+
conn.allowHalfOpen = true;
|
|
146
149
|
const id = NEXT_SOCKET_ID++;
|
|
147
150
|
sockets.set(id, {
|
|
148
151
|
handle: conn._handle,
|
|
@@ -151,6 +154,8 @@ async function handleTcpListen({ socketId, stream }) {
|
|
|
151
154
|
tcp: conn,
|
|
152
155
|
server: null,
|
|
153
156
|
localAddress: makeIpAddress(family, conn.localAddress, conn.localPort),
|
|
157
|
+
disposed: false,
|
|
158
|
+
activeStreams: 0,
|
|
154
159
|
});
|
|
155
160
|
writer.write({ family, socketId: id });
|
|
156
161
|
});
|
|
@@ -162,19 +167,111 @@ async function handleTcpListen({ socketId, stream }) {
|
|
|
162
167
|
|
|
163
168
|
async function handleTcpSend({ socketId, stream }) {
|
|
164
169
|
const socket = sockets.get(socketId);
|
|
170
|
+
socket.activeStreams++;
|
|
171
|
+
|
|
165
172
|
const { tcp } = socket;
|
|
166
173
|
const readable = Readable.fromWeb(stream);
|
|
167
174
|
|
|
168
|
-
|
|
169
|
-
|
|
175
|
+
try {
|
|
176
|
+
await pipeline(readable, tcp);
|
|
177
|
+
} finally {
|
|
178
|
+
socket.activeStreams--;
|
|
179
|
+
cleanupDisposedSocket(socketId, socket);
|
|
180
|
+
}
|
|
170
181
|
}
|
|
171
182
|
|
|
172
183
|
async function handleTcpReceive({ socketId, stream }) {
|
|
173
184
|
const socket = sockets.get(socketId);
|
|
185
|
+
const writer = stream.getWriter();
|
|
186
|
+
socket.activeStreams++;
|
|
187
|
+
|
|
174
188
|
const { tcp } = socket;
|
|
175
189
|
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
try {
|
|
191
|
+
await new Promise((resolve, reject) => {
|
|
192
|
+
let settled = false;
|
|
193
|
+
let pending = Promise.resolve();
|
|
194
|
+
|
|
195
|
+
const cleanup = () => {
|
|
196
|
+
tcp.off("data", onData);
|
|
197
|
+
tcp.off("end", onEnd);
|
|
198
|
+
tcp.off("error", onError);
|
|
199
|
+
};
|
|
200
|
+
const settle = (err) => {
|
|
201
|
+
if (settled) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
settled = true;
|
|
205
|
+
cleanup();
|
|
206
|
+
if (err) {
|
|
207
|
+
reject(err);
|
|
208
|
+
} else {
|
|
209
|
+
resolve();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const onData = (chunk) => {
|
|
213
|
+
tcp.pause();
|
|
214
|
+
pending = pending.then(async () => {
|
|
215
|
+
try {
|
|
216
|
+
await writer.write(chunk);
|
|
217
|
+
} catch {
|
|
218
|
+
settle();
|
|
219
|
+
return;
|
|
220
|
+
} finally {
|
|
221
|
+
// Always undo the pause, even if the guest dropped or cancelled the
|
|
222
|
+
// receive stream. Otherwise the send path can remain backpressured
|
|
223
|
+
// forever.
|
|
224
|
+
tcp.resume();
|
|
225
|
+
}
|
|
226
|
+
}, settle);
|
|
227
|
+
};
|
|
228
|
+
const onEnd = () => {
|
|
229
|
+
pending = pending.then(async () => {
|
|
230
|
+
try {
|
|
231
|
+
await writer.close();
|
|
232
|
+
} catch {
|
|
233
|
+
// The guest can drop the receive stream before remote EOF.
|
|
234
|
+
}
|
|
235
|
+
settle();
|
|
236
|
+
}, settle);
|
|
237
|
+
};
|
|
238
|
+
const onError = (err) => {
|
|
239
|
+
pending = pending.finally(() => settle(err));
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
writer.closed.then(
|
|
243
|
+
() => settle(),
|
|
244
|
+
() => settle(),
|
|
245
|
+
);
|
|
246
|
+
tcp.on("data", onData);
|
|
247
|
+
tcp.once("end", onEnd);
|
|
248
|
+
tcp.once("error", onError);
|
|
249
|
+
tcp.resume();
|
|
250
|
+
});
|
|
251
|
+
} finally {
|
|
252
|
+
writer.releaseLock();
|
|
253
|
+
socket.activeStreams--;
|
|
254
|
+
cleanupDisposedSocket(socketId, socket);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cleanupDisposedSocket(socketId, socket) {
|
|
259
|
+
if (!socket.disposed || socket.activeStreams > 0) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (socket.server) {
|
|
264
|
+
socket.server.close();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (socket.tcp) {
|
|
268
|
+
socket.tcp.destroy();
|
|
269
|
+
}
|
|
270
|
+
if (socket.handle) {
|
|
271
|
+
socket.handle.close();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
sockets.delete(socketId);
|
|
178
275
|
}
|
|
179
276
|
|
|
180
277
|
async function handleGetLocalAddress({ socketId }) {
|
|
@@ -241,18 +338,8 @@ function handleTcpDispose({ socketId }) {
|
|
|
241
338
|
return;
|
|
242
339
|
}
|
|
243
340
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (socket.tcp) {
|
|
249
|
-
socket.tcp.destroy();
|
|
250
|
-
}
|
|
251
|
-
if (socket.handle) {
|
|
252
|
-
socket.handle.close();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
sockets.delete(socketId);
|
|
341
|
+
socket.disposed = true;
|
|
342
|
+
cleanupDisposedSocket(socketId, socket);
|
|
256
343
|
}
|
|
257
344
|
|
|
258
345
|
let _recvBufferSize, _sendBufferSize;
|
package/package.json
CHANGED