@bytecodealliance/preview2-shim 0.23.0 → 0.24.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 +148 -7
- package/dist/browser/cli.d.ts +22 -1
- package/dist/browser/cli.js +66 -34
- package/dist/browser/clocks.js +24 -5
- package/dist/browser/common.d.ts +5 -0
- package/dist/browser/common.js +16 -0
- package/dist/browser/filesystem.d.ts +55 -97
- package/dist/browser/filesystem.js +106 -287
- package/dist/browser/http.d.ts +9 -0
- package/dist/browser/http.js +446 -136
- package/dist/browser/in-memory-filesystem.d.ts +22 -0
- package/dist/browser/in-memory-filesystem.js +530 -0
- package/dist/browser/in-memory-http.d.ts +8 -0
- package/dist/browser/in-memory-http.js +11 -0
- package/dist/browser/in-memory-sockets.d.ts +34 -0
- package/dist/browser/in-memory-sockets.js +294 -0
- package/dist/browser/io.d.ts +11 -2
- package/dist/browser/io.js +200 -39
- package/dist/browser/random.js +5 -3
- package/dist/browser/sockets.d.ts +2 -1
- package/dist/browser/sockets.js +75 -56
- package/dist/common/instantiation.d.ts +4 -1
- package/dist/common/instantiation.js +85 -49
- package/dist/io/worker-socket-tcp.js +3 -3
- package/dist/io/worker-socket-udp.js +3 -4
- package/dist/io/worker-sockets.d.ts +4 -4
- package/dist/io/worker-sockets.js +29 -9
- package/dist/io/worker-thread.bundle.js +22 -9
- package/dist/nodejs/filesystem.d.ts +7 -0
- package/dist/nodejs/filesystem.js +65 -22
- package/dist/nodejs/sockets.d.ts +13 -0
- package/dist/nodejs/sockets.js +20 -0
- package/package.json +1 -1
- package/types/instantiation.d.ts +64 -12
package/dist/browser/http.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { inputStreamCreate, outputStreamCreate, pollableCreate } from "./io.js";
|
|
1
|
+
import { inputStreamCreate, ioErrorCreate, outputStreamCreate, pollableCreate } from "./io.js";
|
|
2
|
+
export { InMemoryHttpClient } from "./in-memory-http.js";
|
|
2
3
|
const symbolDispose = Symbol.dispose || Symbol.for("dispose");
|
|
4
|
+
const utf8Encoder = new TextEncoder();
|
|
3
5
|
const utf8Decoder = new TextDecoder();
|
|
4
6
|
const forbiddenHeaders = new Set(["connection", "keep-alive", "host"]);
|
|
5
7
|
const DEFAULT_HTTP_TIMEOUT_NS = 600000000000n;
|
|
6
8
|
// RFC 9110 compliant header validation
|
|
7
9
|
const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
8
10
|
const FIELD_VALUE_RE = /^[\t\x20-\x7E\x80-\xFF]*$/;
|
|
11
|
+
const BRACKETED_IPV6_AUTHORITY_RE = /^\[([0-9A-Fa-f:.]+)\](?::([0-9]+))?$/;
|
|
12
|
+
const DNS_OR_IPV4_AUTHORITY_RE = /^([a-zA-Z0-9.-]+)(?::([0-9]+))?$/;
|
|
9
13
|
function validateHeaderName(name) {
|
|
10
14
|
if (!TOKEN_RE.test(name)) {
|
|
11
15
|
throw { tag: "invalid-syntax" };
|
|
@@ -142,14 +146,14 @@ const fieldsFromEntriesChecked = Fields._fromEntriesChecked;
|
|
|
142
146
|
// @ts-expect-error - Deleting static method
|
|
143
147
|
delete Fields._fromEntriesChecked;
|
|
144
148
|
class RequestOptions {
|
|
145
|
-
#connectTimeout
|
|
146
|
-
#firstByteTimeout
|
|
147
|
-
#betweenBytesTimeout
|
|
149
|
+
#connectTimeout;
|
|
150
|
+
#firstByteTimeout;
|
|
151
|
+
#betweenBytesTimeout;
|
|
148
152
|
connectTimeout() {
|
|
149
153
|
return this.#connectTimeout;
|
|
150
154
|
}
|
|
151
155
|
setConnectTimeout(duration) {
|
|
152
|
-
if (duration < 0n) {
|
|
156
|
+
if (duration !== undefined && duration < 0n) {
|
|
153
157
|
throw new Error("duration must not be negative");
|
|
154
158
|
}
|
|
155
159
|
this.#connectTimeout = duration;
|
|
@@ -158,7 +162,7 @@ class RequestOptions {
|
|
|
158
162
|
return this.#firstByteTimeout;
|
|
159
163
|
}
|
|
160
164
|
setFirstByteTimeout(duration) {
|
|
161
|
-
if (duration < 0n) {
|
|
165
|
+
if (duration !== undefined && duration < 0n) {
|
|
162
166
|
throw new Error("duration must not be negative");
|
|
163
167
|
}
|
|
164
168
|
this.#firstByteTimeout = duration;
|
|
@@ -167,7 +171,7 @@ class RequestOptions {
|
|
|
167
171
|
return this.#betweenBytesTimeout;
|
|
168
172
|
}
|
|
169
173
|
setBetweenBytesTimeout(duration) {
|
|
170
|
-
if (duration < 0n) {
|
|
174
|
+
if (duration !== undefined && duration < 0n) {
|
|
171
175
|
throw new Error("duration must not be negative");
|
|
172
176
|
}
|
|
173
177
|
this.#betweenBytesTimeout = duration;
|
|
@@ -177,6 +181,11 @@ class OutgoingBody {
|
|
|
177
181
|
#outputStream = null;
|
|
178
182
|
#chunks = [];
|
|
179
183
|
#finished = false;
|
|
184
|
+
#resolveFinished;
|
|
185
|
+
#finishedPromise = new Promise((resolve) => (this.#resolveFinished = resolve));
|
|
186
|
+
#requestStream = null;
|
|
187
|
+
#requestStreamController = null;
|
|
188
|
+
#requestStreamCancelled = false;
|
|
180
189
|
write() {
|
|
181
190
|
const outputStream = this.#outputStream;
|
|
182
191
|
if (outputStream === null) {
|
|
@@ -193,29 +202,66 @@ class OutgoingBody {
|
|
|
193
202
|
throw { tag: "internal-error", val: "body already finished" };
|
|
194
203
|
}
|
|
195
204
|
body.#finished = true;
|
|
205
|
+
if (!body.#requestStreamCancelled) {
|
|
206
|
+
body.#requestStreamController?.close();
|
|
207
|
+
}
|
|
208
|
+
body.#resolveFinished();
|
|
196
209
|
}
|
|
197
|
-
|
|
198
|
-
if (
|
|
210
|
+
#bodyData() {
|
|
211
|
+
if (this.#chunks.length === 0) {
|
|
199
212
|
return null;
|
|
200
213
|
}
|
|
201
214
|
let totalLen = 0;
|
|
202
|
-
for (const chunk of
|
|
215
|
+
for (const chunk of this.#chunks) {
|
|
203
216
|
totalLen += chunk.byteLength;
|
|
204
217
|
}
|
|
205
218
|
const result = new Uint8Array(totalLen);
|
|
206
219
|
let offset = 0;
|
|
207
|
-
for (const chunk of
|
|
220
|
+
for (const chunk of this.#chunks) {
|
|
208
221
|
result.set(chunk, offset);
|
|
209
222
|
offset += chunk.byteLength;
|
|
210
223
|
}
|
|
211
224
|
return result;
|
|
212
225
|
}
|
|
226
|
+
static async _finishedBodyData(outgoingBody) {
|
|
227
|
+
await outgoingBody.#finishedPromise;
|
|
228
|
+
return outgoingBody.#bodyData();
|
|
229
|
+
}
|
|
230
|
+
static _requestBodyStream(outgoingBody) {
|
|
231
|
+
if (outgoingBody.#requestStream === null) {
|
|
232
|
+
outgoingBody.#requestStream = new ReadableStream({
|
|
233
|
+
start(controller) {
|
|
234
|
+
outgoingBody.#requestStreamController = controller;
|
|
235
|
+
for (const chunk of outgoingBody.#chunks) {
|
|
236
|
+
controller.enqueue(chunk);
|
|
237
|
+
}
|
|
238
|
+
outgoingBody.#chunks.length = 0;
|
|
239
|
+
if (outgoingBody.#finished) {
|
|
240
|
+
controller.close();
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
cancel() {
|
|
244
|
+
outgoingBody.#requestStreamCancelled = true;
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return outgoingBody.#requestStream;
|
|
249
|
+
}
|
|
213
250
|
static _create() {
|
|
214
251
|
const outgoingBody = new OutgoingBody();
|
|
215
252
|
const chunks = outgoingBody.#chunks;
|
|
216
253
|
outgoingBody.#outputStream = outputStreamCreate({
|
|
217
254
|
write(buf) {
|
|
218
|
-
|
|
255
|
+
if (outgoingBody.#finished || outgoingBody.#requestStreamCancelled) {
|
|
256
|
+
throw { tag: "closed" };
|
|
257
|
+
}
|
|
258
|
+
const chunk = new Uint8Array(buf);
|
|
259
|
+
if (outgoingBody.#requestStreamController) {
|
|
260
|
+
outgoingBody.#requestStreamController.enqueue(chunk);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
chunks.push(chunk);
|
|
264
|
+
}
|
|
219
265
|
},
|
|
220
266
|
blockingFlush() { },
|
|
221
267
|
subscribe() {
|
|
@@ -229,9 +275,12 @@ class OutgoingBody {
|
|
|
229
275
|
const outgoingBodyCreate = OutgoingBody._create;
|
|
230
276
|
// @ts-expect-error - Deleting static method
|
|
231
277
|
delete OutgoingBody._create;
|
|
232
|
-
const
|
|
278
|
+
const outgoingBodyFinishedData = OutgoingBody._finishedBodyData;
|
|
233
279
|
// @ts-expect-error - Deleting static method
|
|
234
|
-
delete OutgoingBody.
|
|
280
|
+
delete OutgoingBody._finishedBodyData;
|
|
281
|
+
const outgoingBodyRequestStream = OutgoingBody._requestBodyStream;
|
|
282
|
+
// @ts-expect-error - Deleting static method
|
|
283
|
+
delete OutgoingBody._requestBodyStream;
|
|
235
284
|
class OutgoingRequest {
|
|
236
285
|
#method = { tag: "get" };
|
|
237
286
|
#scheme = undefined;
|
|
@@ -283,12 +332,20 @@ class OutgoingRequest {
|
|
|
283
332
|
return this.#authority;
|
|
284
333
|
}
|
|
285
334
|
setAuthority(authority) {
|
|
286
|
-
if (authority) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
335
|
+
if (authority !== undefined) {
|
|
336
|
+
const match = authority.startsWith("[")
|
|
337
|
+
? authority.match(BRACKETED_IPV6_AUTHORITY_RE)
|
|
338
|
+
: authority.match(DNS_OR_IPV4_AUTHORITY_RE);
|
|
339
|
+
if (!match || (match[2] !== undefined && Number(match[2]) > 65535)) {
|
|
340
|
+
throw undefined;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const parsed = new URL(`http://${authority}/`);
|
|
344
|
+
if (parsed.username || parsed.password || !parsed.hostname) {
|
|
345
|
+
throw undefined;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
292
349
|
throw undefined;
|
|
293
350
|
}
|
|
294
351
|
}
|
|
@@ -298,7 +355,7 @@ class OutgoingRequest {
|
|
|
298
355
|
return this.#headers;
|
|
299
356
|
}
|
|
300
357
|
[symbolDispose]() { }
|
|
301
|
-
static _handle(request, options) {
|
|
358
|
+
static _handle(request, options, config = {}) {
|
|
302
359
|
const scheme = schemeString(request.#scheme);
|
|
303
360
|
const method = "val" in request.#method ? request.#method.val : request.#method.tag;
|
|
304
361
|
if (!request.#pathWithQuery) {
|
|
@@ -309,10 +366,16 @@ class OutgoingRequest {
|
|
|
309
366
|
for (const [key, value] of request.#headers.entries()) {
|
|
310
367
|
const lowerKey = key.toLowerCase();
|
|
311
368
|
if (!forbiddenHeaders.has(lowerKey)) {
|
|
312
|
-
headers.
|
|
369
|
+
headers.append(key, utf8Decoder.decode(value));
|
|
313
370
|
}
|
|
314
371
|
}
|
|
315
|
-
|
|
372
|
+
// Request streams are opt-in because Firefox and Safari do not yet support them.
|
|
373
|
+
// The portable default buffers until the guest explicitly finishes the body.
|
|
374
|
+
const bodyData = request.#bodyRequested
|
|
375
|
+
? config.streamingRequestBodies
|
|
376
|
+
? outgoingBodyRequestStream(request.#body)
|
|
377
|
+
: outgoingBodyFinishedData(request.#body)
|
|
378
|
+
: null;
|
|
316
379
|
let timeoutMs = Number(DEFAULT_HTTP_TIMEOUT_NS / 1000000n);
|
|
317
380
|
if (options) {
|
|
318
381
|
const ct = options.connectTimeout?.() ?? DEFAULT_HTTP_TIMEOUT_NS;
|
|
@@ -328,7 +391,7 @@ const outgoingRequestHandle = OutgoingRequest._handle;
|
|
|
328
391
|
delete OutgoingRequest._handle;
|
|
329
392
|
class IncomingBody {
|
|
330
393
|
#finished = false;
|
|
331
|
-
#stream =
|
|
394
|
+
#stream = null;
|
|
332
395
|
stream() {
|
|
333
396
|
if (!this.#stream) {
|
|
334
397
|
throw undefined;
|
|
@@ -345,115 +408,130 @@ class IncomingBody {
|
|
|
345
408
|
return futureTrailersCreate();
|
|
346
409
|
}
|
|
347
410
|
[symbolDispose]() { }
|
|
348
|
-
static _create(fetchResponse) {
|
|
411
|
+
static _create(fetchResponse, bufferedBody) {
|
|
349
412
|
const incomingBody = new IncomingBody();
|
|
350
|
-
let buffer = null;
|
|
413
|
+
let buffer = bufferedBody ?? null;
|
|
351
414
|
let bufferOffset = 0;
|
|
352
|
-
let done =
|
|
415
|
+
let done = bufferedBody !== undefined;
|
|
353
416
|
let reader = null;
|
|
354
417
|
let readPromise = null;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
418
|
+
let readError = null;
|
|
419
|
+
let disposed = false;
|
|
420
|
+
function ready() {
|
|
421
|
+
return done || (buffer !== null && bufferOffset < buffer.byteLength);
|
|
359
422
|
}
|
|
360
423
|
function startRead() {
|
|
361
|
-
if (readPromise ||
|
|
424
|
+
if (readPromise || ready()) {
|
|
362
425
|
return;
|
|
363
426
|
}
|
|
364
|
-
|
|
365
|
-
if (!reader) {
|
|
427
|
+
if (!fetchResponse.body) {
|
|
366
428
|
done = true;
|
|
367
429
|
return;
|
|
368
430
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
readPromise = null;
|
|
380
|
-
done = true;
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
incomingBody.#stream = inputStreamCreate({
|
|
384
|
-
read(len) {
|
|
385
|
-
if (done && (buffer === null || bufferOffset >= buffer.byteLength)) {
|
|
386
|
-
throw { tag: "closed" };
|
|
387
|
-
}
|
|
388
|
-
if (buffer !== null && bufferOffset < buffer.byteLength) {
|
|
389
|
-
const available = buffer.byteLength - bufferOffset;
|
|
390
|
-
const toRead = Math.min(Number(len), available);
|
|
391
|
-
const slice = buffer.slice(bufferOffset, bufferOffset + toRead);
|
|
392
|
-
bufferOffset += toRead;
|
|
393
|
-
if (bufferOffset >= buffer.byteLength) {
|
|
394
|
-
buffer = null;
|
|
395
|
-
bufferOffset = 0;
|
|
396
|
-
if (!done) {
|
|
397
|
-
startRead();
|
|
431
|
+
reader ??= fetchResponse.body.getReader();
|
|
432
|
+
const activeReader = reader;
|
|
433
|
+
readPromise = (async () => {
|
|
434
|
+
try {
|
|
435
|
+
// Empty Fetch chunks do not make a WASI input stream readable.
|
|
436
|
+
// Keep a single read in flight and buffer at most one nonempty chunk.
|
|
437
|
+
while (!done) {
|
|
438
|
+
const result = await activeReader.read();
|
|
439
|
+
if (disposed) {
|
|
440
|
+
return;
|
|
398
441
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
throw { tag: "closed" };
|
|
407
|
-
}
|
|
408
|
-
if (buffer !== null && bufferOffset < buffer.byteLength) {
|
|
409
|
-
const available = buffer.byteLength - bufferOffset;
|
|
410
|
-
const toRead = Math.min(Number(len), available);
|
|
411
|
-
const slice = buffer.slice(bufferOffset, bufferOffset + toRead);
|
|
412
|
-
bufferOffset += toRead;
|
|
413
|
-
if (bufferOffset >= buffer.byteLength) {
|
|
414
|
-
buffer = null;
|
|
415
|
-
bufferOffset = 0;
|
|
416
|
-
if (!done) {
|
|
417
|
-
startRead();
|
|
442
|
+
if (result.done) {
|
|
443
|
+
done = true;
|
|
444
|
+
}
|
|
445
|
+
else if (result.value.byteLength > 0) {
|
|
446
|
+
buffer = result.value;
|
|
447
|
+
bufferOffset = 0;
|
|
448
|
+
return;
|
|
418
449
|
}
|
|
419
450
|
}
|
|
420
|
-
return slice;
|
|
421
451
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
throw { tag: "closed" };
|
|
452
|
+
catch (cause) {
|
|
453
|
+
done = true;
|
|
454
|
+
if (!disposed) {
|
|
455
|
+
readError = ioErrorCreate(cause instanceof Error ? cause.message : String(cause));
|
|
427
456
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (bufferOffset >= buffer.byteLength) {
|
|
434
|
-
buffer = null;
|
|
435
|
-
bufferOffset = 0;
|
|
436
|
-
if (!done) {
|
|
437
|
-
startRead();
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return slice;
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
readPromise = null;
|
|
460
|
+
if (done) {
|
|
461
|
+
activeReader.releaseLock();
|
|
441
462
|
}
|
|
442
|
-
throw { tag: "closed" };
|
|
443
|
-
});
|
|
444
|
-
},
|
|
445
|
-
subscribe() {
|
|
446
|
-
if (done || (buffer !== null && bufferOffset < buffer.byteLength)) {
|
|
447
|
-
return pollableCreate();
|
|
448
463
|
}
|
|
464
|
+
})();
|
|
465
|
+
}
|
|
466
|
+
async function waitForReadable() {
|
|
467
|
+
while (!ready()) {
|
|
468
|
+
startRead();
|
|
469
|
+
await readPromise;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function read(len) {
|
|
473
|
+
if (readError) {
|
|
474
|
+
const error = readError;
|
|
475
|
+
readError = null;
|
|
476
|
+
throw { tag: "last-operation-failed", val: error };
|
|
477
|
+
}
|
|
478
|
+
if (done && (buffer === null || bufferOffset >= buffer.byteLength)) {
|
|
479
|
+
throw { tag: "closed" };
|
|
480
|
+
}
|
|
481
|
+
if (buffer === null || bufferOffset >= buffer.byteLength) {
|
|
449
482
|
startRead();
|
|
450
|
-
|
|
451
|
-
|
|
483
|
+
return new Uint8Array(0);
|
|
484
|
+
}
|
|
485
|
+
const toRead = Math.min(Number(len), buffer.byteLength - bufferOffset);
|
|
486
|
+
const slice = buffer.slice(bufferOffset, bufferOffset + toRead);
|
|
487
|
+
bufferOffset += toRead;
|
|
488
|
+
if (bufferOffset >= buffer.byteLength) {
|
|
489
|
+
buffer = null;
|
|
490
|
+
bufferOffset = 0;
|
|
491
|
+
startRead();
|
|
492
|
+
}
|
|
493
|
+
return slice;
|
|
494
|
+
}
|
|
495
|
+
function blockingRead(len) {
|
|
496
|
+
if (len === 0n || ready()) {
|
|
497
|
+
return read(len);
|
|
498
|
+
}
|
|
499
|
+
return waitForReadable().then(() => read(len));
|
|
500
|
+
}
|
|
501
|
+
function blockingSkip(len) {
|
|
502
|
+
const result = blockingRead(len);
|
|
503
|
+
return result instanceof Promise
|
|
504
|
+
? result.then((bytes) => BigInt(bytes.byteLength))
|
|
505
|
+
: BigInt(result.byteLength);
|
|
506
|
+
}
|
|
507
|
+
incomingBody.#stream = inputStreamCreate({
|
|
508
|
+
read,
|
|
509
|
+
// WIT declares synchronous signatures; JSPI awaits these host results
|
|
510
|
+
// for blocking imports. Preserve synchronous results for buffered bodies.
|
|
511
|
+
blockingRead: blockingRead,
|
|
512
|
+
blockingSkip: blockingSkip,
|
|
513
|
+
subscribe() {
|
|
514
|
+
return pollableCreate({ ready, wait: waitForReadable });
|
|
515
|
+
},
|
|
516
|
+
drop() {
|
|
517
|
+
disposed = true;
|
|
518
|
+
done = true;
|
|
519
|
+
buffer = null;
|
|
520
|
+
readError = null;
|
|
521
|
+
if (reader) {
|
|
522
|
+
const activeReader = reader;
|
|
523
|
+
void activeReader
|
|
524
|
+
.cancel()
|
|
525
|
+
.catch(() => { })
|
|
526
|
+
.finally(() => {
|
|
527
|
+
activeReader.releaseLock();
|
|
528
|
+
});
|
|
452
529
|
}
|
|
453
|
-
return pollableCreate();
|
|
454
530
|
},
|
|
455
531
|
});
|
|
456
|
-
|
|
532
|
+
if (bufferedBody === undefined) {
|
|
533
|
+
startRead();
|
|
534
|
+
}
|
|
457
535
|
return incomingBody;
|
|
458
536
|
}
|
|
459
537
|
}
|
|
@@ -495,6 +573,144 @@ class IncomingResponse {
|
|
|
495
573
|
const incomingResponseCreate = IncomingResponse._create;
|
|
496
574
|
// @ts-expect-error - Deleting static method
|
|
497
575
|
delete IncomingResponse._create;
|
|
576
|
+
class IncomingRequest {
|
|
577
|
+
#request;
|
|
578
|
+
#headers;
|
|
579
|
+
#body;
|
|
580
|
+
method() {
|
|
581
|
+
const method = this.#request.method.toLowerCase();
|
|
582
|
+
return { tag: method };
|
|
583
|
+
}
|
|
584
|
+
pathWithQuery() {
|
|
585
|
+
const url = new URL(this.#request.url);
|
|
586
|
+
return `${url.pathname}${url.search}`;
|
|
587
|
+
}
|
|
588
|
+
scheme() {
|
|
589
|
+
const protocol = new URL(this.#request.url).protocol;
|
|
590
|
+
if (protocol === "http:") {
|
|
591
|
+
return { tag: "HTTP" };
|
|
592
|
+
}
|
|
593
|
+
if (protocol === "https:") {
|
|
594
|
+
return { tag: "HTTPS" };
|
|
595
|
+
}
|
|
596
|
+
return { tag: "other", val: protocol.slice(0, -1) };
|
|
597
|
+
}
|
|
598
|
+
authority() {
|
|
599
|
+
return new URL(this.#request.url).host;
|
|
600
|
+
}
|
|
601
|
+
headers() {
|
|
602
|
+
return this.#headers;
|
|
603
|
+
}
|
|
604
|
+
consume() {
|
|
605
|
+
if (!this.#body) {
|
|
606
|
+
throw new Error("incoming request body already consumed");
|
|
607
|
+
}
|
|
608
|
+
const body = this.#body;
|
|
609
|
+
this.#body = undefined;
|
|
610
|
+
return body;
|
|
611
|
+
}
|
|
612
|
+
static _create(request, bufferedBody) {
|
|
613
|
+
const incoming = new IncomingRequest();
|
|
614
|
+
incoming.#request = request.clone();
|
|
615
|
+
const encoder = new TextEncoder();
|
|
616
|
+
incoming.#headers = fieldsLock(fieldsFromEntriesChecked([...request.headers.entries()].map(([name, value]) => [
|
|
617
|
+
name,
|
|
618
|
+
encoder.encode(value),
|
|
619
|
+
])));
|
|
620
|
+
incoming.#body = incomingBodyCreate(new Response(request.body), bufferedBody);
|
|
621
|
+
return incoming;
|
|
622
|
+
}
|
|
623
|
+
static _toRequest(request) {
|
|
624
|
+
return request.#request;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const incomingRequestCreate = IncomingRequest._create;
|
|
628
|
+
// @ts-expect-error - Deleting static method
|
|
629
|
+
delete IncomingRequest._create;
|
|
630
|
+
const incomingRequestToRequest = IncomingRequest._toRequest;
|
|
631
|
+
// @ts-expect-error - Deleting static method
|
|
632
|
+
delete IncomingRequest._toRequest;
|
|
633
|
+
class OutgoingResponse {
|
|
634
|
+
#headers;
|
|
635
|
+
#status = 200;
|
|
636
|
+
#body = outgoingBodyCreate();
|
|
637
|
+
#bodyRequested = false;
|
|
638
|
+
constructor(headers) {
|
|
639
|
+
fieldsLock(headers);
|
|
640
|
+
this.#headers = headers;
|
|
641
|
+
}
|
|
642
|
+
statusCode() {
|
|
643
|
+
return this.#status;
|
|
644
|
+
}
|
|
645
|
+
setStatusCode(statusCode) {
|
|
646
|
+
if (!Number.isInteger(statusCode) || statusCode < 100 || statusCode > 999) {
|
|
647
|
+
throw new TypeError("invalid HTTP status code");
|
|
648
|
+
}
|
|
649
|
+
this.#status = statusCode;
|
|
650
|
+
}
|
|
651
|
+
headers() {
|
|
652
|
+
return this.#headers;
|
|
653
|
+
}
|
|
654
|
+
body() {
|
|
655
|
+
if (this.#bodyRequested) {
|
|
656
|
+
throw new Error("outgoing response body already requested");
|
|
657
|
+
}
|
|
658
|
+
this.#bodyRequested = true;
|
|
659
|
+
return this.#body;
|
|
660
|
+
}
|
|
661
|
+
static async _toResponse(response) {
|
|
662
|
+
const headers = new Headers();
|
|
663
|
+
for (const [name, value] of response.#headers.entries()) {
|
|
664
|
+
headers.append(name, utf8Decoder.decode(value));
|
|
665
|
+
}
|
|
666
|
+
const body = response.#bodyRequested
|
|
667
|
+
? await outgoingBodyFinishedData(response.#body)
|
|
668
|
+
: null;
|
|
669
|
+
return new Response(body, {
|
|
670
|
+
status: response.#status,
|
|
671
|
+
headers,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const outgoingResponseToResponse = OutgoingResponse._toResponse;
|
|
676
|
+
// @ts-expect-error - Deleting static method
|
|
677
|
+
delete OutgoingResponse._toResponse;
|
|
678
|
+
class ResponseOutparam {
|
|
679
|
+
#used = false;
|
|
680
|
+
#resolve;
|
|
681
|
+
#reject;
|
|
682
|
+
static set(param, response) {
|
|
683
|
+
if (param.#used) {
|
|
684
|
+
throw new Error("response outparam already set");
|
|
685
|
+
}
|
|
686
|
+
param.#used = true;
|
|
687
|
+
if (response.tag === "ok") {
|
|
688
|
+
void outgoingResponseToResponse(response.val).then(param.#resolve, param.#reject);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
param.#resolve(new Response(`WASI HTTP handler error: ${JSON.stringify(response.val)}`, {
|
|
692
|
+
status: 500,
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
static _isUsed(param) {
|
|
697
|
+
return param.#used;
|
|
698
|
+
}
|
|
699
|
+
static _create() {
|
|
700
|
+
const param = new ResponseOutparam();
|
|
701
|
+
const response = new Promise((resolve, reject) => {
|
|
702
|
+
param.#resolve = resolve;
|
|
703
|
+
param.#reject = reject;
|
|
704
|
+
});
|
|
705
|
+
return [param, response];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const responseOutparamCreate = ResponseOutparam._create;
|
|
709
|
+
// @ts-expect-error - Deleting static method
|
|
710
|
+
delete ResponseOutparam._create;
|
|
711
|
+
const responseOutparamIsUsed = ResponseOutparam._isUsed;
|
|
712
|
+
// @ts-expect-error - Deleting static method
|
|
713
|
+
delete ResponseOutparam._isUsed;
|
|
498
714
|
class FutureTrailers {
|
|
499
715
|
#requested = false;
|
|
500
716
|
subscribe() {
|
|
@@ -524,14 +740,13 @@ function mapFetchError(err) {
|
|
|
524
740
|
if (err.name === "AbortError") {
|
|
525
741
|
return { tag: "connection-timeout" };
|
|
526
742
|
}
|
|
527
|
-
if (err.name === "TypeError") {
|
|
528
|
-
return { tag: "connection-refused" };
|
|
529
|
-
}
|
|
530
743
|
return { tag: "internal-error", val: err.message };
|
|
531
744
|
}
|
|
532
745
|
class FutureIncomingResponse {
|
|
533
746
|
#result = undefined;
|
|
534
747
|
#promise = null;
|
|
748
|
+
#controller = null;
|
|
749
|
+
#settled = false;
|
|
535
750
|
subscribe() {
|
|
536
751
|
return pollableCreate(this.#promise);
|
|
537
752
|
}
|
|
@@ -541,30 +756,54 @@ class FutureIncomingResponse {
|
|
|
541
756
|
}
|
|
542
757
|
const result = this.#result;
|
|
543
758
|
this.#result = { tag: "err" };
|
|
759
|
+
// The returned response now owns the body. Dropping this consumed future
|
|
760
|
+
// must not abort a Fetch body that the guest is still streaming.
|
|
761
|
+
if (result.tag === "ok" && result.val.tag === "ok") {
|
|
762
|
+
this.#controller = null;
|
|
763
|
+
}
|
|
544
764
|
return result;
|
|
545
765
|
}
|
|
546
766
|
[symbolDispose]() {
|
|
767
|
+
// Only abort if the request never settled. If fetch already
|
|
768
|
+
// resolved (even with an ok response whose body is still being
|
|
769
|
+
// streamed elsewhere), aborting here kills the underlying
|
|
770
|
+
// connection and body stream out from under whoever is still
|
|
771
|
+
// reading it, surfacing as a bogus "BodyStreamBuffer was aborted"
|
|
772
|
+
// error on a request that actually succeeded.
|
|
773
|
+
if (!this.#settled) {
|
|
774
|
+
this.#controller?.abort();
|
|
775
|
+
}
|
|
776
|
+
this.#controller = null;
|
|
547
777
|
this.#promise = null;
|
|
548
778
|
}
|
|
549
779
|
static _create(url, method, headers, bodyData, timeoutMs) {
|
|
550
780
|
const future = new FutureIncomingResponse();
|
|
551
781
|
const controller = new AbortController();
|
|
782
|
+
future.#controller = controller;
|
|
552
783
|
let timer;
|
|
553
784
|
if (timeoutMs < Infinity) {
|
|
554
785
|
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
555
786
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
787
|
+
future.#promise = Promise.resolve(bodyData)
|
|
788
|
+
.then((bodyData) => {
|
|
789
|
+
const init = {
|
|
790
|
+
method,
|
|
791
|
+
headers,
|
|
792
|
+
signal: controller.signal,
|
|
793
|
+
};
|
|
794
|
+
if (bodyData && method !== "GET" && method !== "HEAD") {
|
|
795
|
+
init.body = bodyData;
|
|
796
|
+
if (bodyData instanceof ReadableStream) {
|
|
797
|
+
init.duplex = "half";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return globalThis.fetch(url, init);
|
|
801
|
+
})
|
|
802
|
+
.then((response) => {
|
|
565
803
|
if (timer) {
|
|
566
804
|
clearTimeout(timer);
|
|
567
805
|
}
|
|
806
|
+
future.#settled = true;
|
|
568
807
|
future.#result = {
|
|
569
808
|
tag: "ok",
|
|
570
809
|
val: {
|
|
@@ -576,6 +815,7 @@ class FutureIncomingResponse {
|
|
|
576
815
|
if (timer) {
|
|
577
816
|
clearTimeout(timer);
|
|
578
817
|
}
|
|
818
|
+
future.#settled = true;
|
|
579
819
|
future.#result = {
|
|
580
820
|
tag: "ok",
|
|
581
821
|
val: {
|
|
@@ -613,31 +853,101 @@ function httpErrorCode(err) {
|
|
|
613
853
|
val: "message" in err ? err.message : err.toDebugString(),
|
|
614
854
|
};
|
|
615
855
|
}
|
|
856
|
+
let requestStreamingEnabled = false;
|
|
857
|
+
/** Enable or disable Fetch `ReadableStream` request bodies. Disabled by default. */
|
|
858
|
+
export function _setRequestStreaming(enabled) {
|
|
859
|
+
if (typeof enabled !== "boolean") {
|
|
860
|
+
throw new TypeError("request streaming setting must be a boolean");
|
|
861
|
+
}
|
|
862
|
+
requestStreamingEnabled = enabled;
|
|
863
|
+
}
|
|
616
864
|
export const outgoingHandler = {
|
|
617
|
-
|
|
618
|
-
|
|
865
|
+
handle(request, options) {
|
|
866
|
+
return outgoingRequestHandle(request, options, {
|
|
867
|
+
streamingRequestBodies: requestStreamingEnabled,
|
|
868
|
+
});
|
|
869
|
+
},
|
|
619
870
|
};
|
|
620
871
|
export const incomingHandler = {
|
|
621
|
-
|
|
622
|
-
|
|
872
|
+
handle() {
|
|
873
|
+
throw "not-supported";
|
|
874
|
+
},
|
|
623
875
|
};
|
|
876
|
+
/** Create a `wasi:http/incoming-handler` namespace backed by a Web Request handler. */
|
|
877
|
+
export function createIncomingHandler(handler) {
|
|
878
|
+
return {
|
|
879
|
+
async handle(request, responseOut) {
|
|
880
|
+
try {
|
|
881
|
+
const response = await handler(incomingRequestToRequest(request));
|
|
882
|
+
if (!(response instanceof Response)) {
|
|
883
|
+
throw new TypeError("incoming HTTP handler must return a Response");
|
|
884
|
+
}
|
|
885
|
+
ResponseOutparam.set(responseOut, {
|
|
886
|
+
tag: "ok",
|
|
887
|
+
val: await responseToOutgoingResponse(response),
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
ResponseOutparam.set(responseOut, {
|
|
892
|
+
tag: "err",
|
|
893
|
+
val: {
|
|
894
|
+
tag: "internal-error",
|
|
895
|
+
val: error instanceof Error ? error.message : String(error),
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
async function responseToOutgoingResponse(response) {
|
|
903
|
+
const headers = [];
|
|
904
|
+
response.headers.forEach((value, name) => headers.push([name, utf8Encoder.encode(value)]));
|
|
905
|
+
const outgoing = new OutgoingResponse(fieldsFromEntriesChecked(headers));
|
|
906
|
+
outgoing.setStatusCode(response.status);
|
|
907
|
+
if (response.body) {
|
|
908
|
+
const body = outgoing.body();
|
|
909
|
+
const stream = body.write();
|
|
910
|
+
const reader = response.body.getReader();
|
|
911
|
+
for (let result = await reader.read(); !result.done; result = await reader.read()) {
|
|
912
|
+
let offset = 0;
|
|
913
|
+
while (offset < result.value.byteLength) {
|
|
914
|
+
const permit = stream.checkWrite();
|
|
915
|
+
if (permit === 0n) {
|
|
916
|
+
await stream.subscribe().block();
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
const length = Math.min(Number(permit), result.value.byteLength - offset);
|
|
920
|
+
stream.write(result.value.subarray(offset, offset + length));
|
|
921
|
+
offset += length;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
OutgoingBody.finish(body, undefined);
|
|
925
|
+
}
|
|
926
|
+
return outgoing;
|
|
927
|
+
}
|
|
928
|
+
/** Translate a browser Request through a WASI handler with a synchronously readable body. */
|
|
929
|
+
export async function handleIncomingRequest(request, handler) {
|
|
930
|
+
const [responseOut, response] = responseOutparamCreate();
|
|
931
|
+
const body = request.body
|
|
932
|
+
? new Uint8Array(await request.clone().arrayBuffer())
|
|
933
|
+
: new Uint8Array();
|
|
934
|
+
await handler(incomingRequestCreate(request, body), responseOut);
|
|
935
|
+
if (!responseOutparamIsUsed(responseOut)) {
|
|
936
|
+
throw new Error("WASI HTTP handler returned without setting its response outparam");
|
|
937
|
+
}
|
|
938
|
+
return response;
|
|
939
|
+
}
|
|
624
940
|
export const types = {
|
|
625
941
|
Fields,
|
|
626
942
|
FutureIncomingResponse,
|
|
627
943
|
FutureTrailers,
|
|
628
944
|
IncomingBody,
|
|
629
|
-
|
|
630
|
-
IncomingRequest: class IncomingRequest {
|
|
631
|
-
},
|
|
945
|
+
IncomingRequest,
|
|
632
946
|
IncomingResponse,
|
|
633
947
|
OutgoingBody,
|
|
634
948
|
OutgoingRequest,
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
},
|
|
638
|
-
// @ts-expect-error Not implemented
|
|
639
|
-
ResponseOutparam: class ResponseOutparam {
|
|
640
|
-
},
|
|
949
|
+
OutgoingResponse,
|
|
950
|
+
ResponseOutparam,
|
|
641
951
|
RequestOptions,
|
|
642
952
|
httpErrorCode,
|
|
643
953
|
};
|