@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.
@@ -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 = DEFAULT_HTTP_TIMEOUT_NS;
146
- #firstByteTimeout = DEFAULT_HTTP_TIMEOUT_NS;
147
- #betweenBytesTimeout = DEFAULT_HTTP_TIMEOUT_NS;
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
- static _bodyData(outgoingBody) {
198
- if (outgoingBody.#chunks.length === 0) {
210
+ #bodyData() {
211
+ if (this.#chunks.length === 0) {
199
212
  return null;
200
213
  }
201
214
  let totalLen = 0;
202
- for (const chunk of outgoingBody.#chunks) {
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 outgoingBody.#chunks) {
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
- chunks.push(new Uint8Array(buf));
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 outgoingBodyData = OutgoingBody._bodyData;
278
+ const outgoingBodyFinishedData = OutgoingBody._finishedBodyData;
233
279
  // @ts-expect-error - Deleting static method
234
- delete OutgoingBody._bodyData;
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 [host, port, ...extra] = authority.split(":");
288
- const portNum = Number(port);
289
- if (extra.length ||
290
- (port !== undefined && (portNum.toString() !== port || portNum > 65535)) ||
291
- !host.match(/^[a-zA-Z0-9-.]+$/)) {
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.set(key, utf8Decoder.decode(value));
369
+ headers.append(key, utf8Decoder.decode(value));
313
370
  }
314
371
  }
315
- const bodyData = outgoingBodyData(request.#body);
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 = undefined;
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 = false;
415
+ let done = bufferedBody !== undefined;
353
416
  let reader = null;
354
417
  let readPromise = null;
355
- function ensureReader() {
356
- if (!reader && fetchResponse.body) {
357
- reader = fetchResponse.body.getReader();
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 || done) {
424
+ if (readPromise || ready()) {
362
425
  return;
363
426
  }
364
- ensureReader();
365
- if (!reader) {
427
+ if (!fetchResponse.body) {
366
428
  done = true;
367
429
  return;
368
430
  }
369
- readPromise = reader.read().then((result) => {
370
- readPromise = null;
371
- if (result.done) {
372
- done = true;
373
- }
374
- else {
375
- buffer = result.value;
376
- bufferOffset = 0;
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
- return slice;
401
- }
402
- throw { tag: "would-block" };
403
- },
404
- blockingRead(len) {
405
- if (done && (buffer === null || bufferOffset >= buffer.byteLength)) {
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
- startRead();
423
- const waitFor = readPromise || Promise.resolve();
424
- return waitFor.then(() => {
425
- if (done && (buffer === null || bufferOffset >= buffer.byteLength)) {
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
- if (buffer !== null && bufferOffset < buffer.byteLength) {
429
- const available = buffer.byteLength - bufferOffset;
430
- const toRead = Math.min(Number(len), available);
431
- const slice = buffer.slice(bufferOffset, bufferOffset + toRead);
432
- bufferOffset += toRead;
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
- if (readPromise) {
451
- return pollableCreate(readPromise);
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
- startRead();
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
- const init = {
557
- method,
558
- headers,
559
- signal: controller.signal,
560
- };
561
- if (bodyData && method !== "GET" && method !== "HEAD") {
562
- init.body = bodyData;
563
- }
564
- future.#promise = fetch(url, init).then((response) => {
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
- // @ts-expect-error Not matching signature in WIT
618
- handle: outgoingRequestHandle,
865
+ handle(request, options) {
866
+ return outgoingRequestHandle(request, options, {
867
+ streamingRequestBodies: requestStreamingEnabled,
868
+ });
869
+ },
619
870
  };
620
871
  export const incomingHandler = {
621
- // Not implemented
622
- handle() { },
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
- // @ts-expect-error Not implemented
630
- IncomingRequest: class IncomingRequest {
631
- },
945
+ IncomingRequest,
632
946
  IncomingResponse,
633
947
  OutgoingBody,
634
948
  OutgoingRequest,
635
- // @ts-expect-error Not implemented
636
- OutgoingResponse: class OutgoingResponse {
637
- },
638
- // @ts-expect-error Not implemented
639
- ResponseOutparam: class ResponseOutparam {
640
- },
949
+ OutgoingResponse,
950
+ ResponseOutparam,
641
951
  RequestOptions,
642
952
  httpErrorCode,
643
953
  };