Skip to content

Commit 260b948

Browse files
refactor(std): reuse request/response parsing impls
1 parent 91cc099 commit 260b948

File tree

7 files changed

+254
-311
lines changed

7 files changed

+254
-311
lines changed
Lines changed: 5 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
/**
2-
* This file implements helpers and relevant types for use with `wasi:http` 0.2.x APIs
2+
* This file implements helpers and relevant types for use with `wasi:http@0.2.3`
33
*
44
* @see: https://github.com/WebAssembly/wasi-http
55
*/
66

7-
import { IncomingBody, IncomingRequest } from "wasi:http/types@0.2.3";
8-
import { Pollable } from "wasi:io/poll@0.2.3";
9-
import { InputStream } from "wasi:io/streams@0.2.3";
7+
import { type IncomingRequest, IncomingBody } from "wasi:http/types@0.2.3";
108

11-
import { ensureGlobalReadableStream, ensureGlobalRequest } from "../../../globals.js";
12-
import { wasiHTTPMethodToString } from "../../../0.2.x/http/index.js";
13-
import { DEFAULT_INCOMING_BODY_READ_MAX_BYTES } from "../../../constants.js";
9+
import { genReadWASIRequestFn } from "../../../0.2.x/http/types/request.js";
1410

1511
/**
1612
* Create a web-platform `Request` from a `wasi:http/incoming-handler` `incoming-request`.
@@ -24,68 +20,6 @@ import { DEFAULT_INCOMING_BODY_READ_MAX_BYTES } from "../../../constants.js";
2420
* @see https://github.com/WebAssembly/wasi-http
2521
*/
2622
export async function readWASIRequest(wasiIncomingRequest: IncomingRequest): Promise<Request> {
27-
// TODO: reuse bytes for subsequent web requests, doing pruning/growing where necessary
28-
// TODO: trailer support
29-
if (!wasiIncomingRequest) {
30-
throw new TypeError('WASI incoming request not provided');
31-
}
32-
const method = wasiHTTPMethodToString(wasiIncomingRequest.method());
33-
const pathWithQuery = wasiIncomingRequest.pathWithQuery();
34-
const scheme = wasiIncomingRequest.scheme();
35-
const authority = wasiIncomingRequest.authority();
36-
const decoder = new TextDecoder('utf-8');
37-
const headers = Object.fromEntries(
38-
wasiIncomingRequest.headers().entries().map(([k,valueBytes]) => {
39-
return [k, decoder.decode(valueBytes)];
40-
})
41-
);
42-
const Request = ensureGlobalRequest();
43-
const ReadableStream = ensureGlobalReadableStream();
44-
45-
let incomingBody: IncomingBody;
46-
let incomingBodyStream: InputStream;
47-
let incomingBodyPollable: Pollable;
48-
const body = new ReadableStream({
49-
async pull(controller) {
50-
if (!incomingBody) {
51-
incomingBody = wasiIncomingRequest.consume();
52-
incomingBodyStream = incomingBody.stream();
53-
incomingBodyPollable = incomingBodyStream.subscribe();
54-
}
55-
56-
// Read all information coming from the request
57-
while (true) {
58-
// Wait until the pollable is ready
59-
if (!incomingBodyPollable.ready()) {
60-
incomingBodyPollable.block();
61-
}
62-
63-
try {
64-
const bytes = incomingBodyStream.read(
65-
DEFAULT_INCOMING_BODY_READ_MAX_BYTES
66-
);
67-
if (bytes.length === 0) {
68-
break;
69-
} else {
70-
controller.enqueue(bytes);
71-
}
72-
} catch (err) {
73-
console.error('error while reading bytes', err);
74-
controller.close();
75-
break;
76-
}
77-
}
78-
79-
incomingBodyPollable[Symbol.dispose]();
80-
incomingBodyStream[Symbol.dispose]();
81-
incomingBody[Symbol.dispose]();
82-
controller.close();
83-
},
84-
});
85-
86-
return new Request(`${scheme}://${authority}/${pathWithQuery}`, {
87-
method,
88-
headers,
89-
body,
90-
});
23+
const f = genReadWASIRequestFn(IncomingBody);
24+
return await f(wasiIncomingRequest);
9125
}
Lines changed: 10 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,15 @@
1-
import { OutgoingBody, ResponseOutparam, Fields, OutgoingResponse, FieldValue } from 'wasi:http/types@0.2.3';
2-
3-
const ENCODER = new TextEncoder();
1+
import { OutgoingBody, ResponseOutparam, Fields, OutgoingResponse } from 'wasi:http/types@0.2.3';
2+
import { genWriteWebResponseFn } from '../../../0.2.x/http/types/response.js';
43

54
/** Write a Web `Response` into a `wasi:http@0.2.x#outgoing-response` */
65
export async function writeWebResponse(resp: Response, outgoingWasiResp: ResponseOutparam) {
7-
// Build headers
8-
const fields: [string, FieldValue][] = [];
9-
for (const [k,v] of [...resp.headers.entries()]) {
10-
fields.push([k.toString(), ENCODER.encode(v)]);
11-
}
12-
const headers = Fields.fromList(fields);
13-
const outgoingResponse = new OutgoingResponse(headers);
14-
15-
// Set status
16-
const status = resp.status;
17-
outgoingResponse.setStatusCode(status);
18-
19-
// Build the outgoing response body
20-
const outgoingBody = outgoingResponse.body();
21-
{
22-
// Create a stream for the response body
23-
const outputStream = outgoingBody.write();
24-
if (resp.body === null) {
25-
throw new Error("unexpectedly missing resp.body");
26-
}
27-
const pollable = outputStream.subscribe();
28-
29-
// Create a reader for the body we'll be writing out
30-
const reader = await resp.body.getReader();
31-
32-
while (true) {
33-
const { value: chunk, done } = await reader.read();
34-
if (done) { break; }
35-
36-
if (chunk.length === 0) {
37-
continue;
38-
}
39-
40-
let written = 0n;
41-
while (written < chunk.length) {
42-
// Wait until output stream is ready
43-
if (!pollable.ready()) {
44-
pollable.block();
45-
}
46-
47-
// Find out how much we are allowed to write
48-
const bytesAllowedRaw = outputStream.checkWrite();
49-
50-
// If we can't write as much as we want, we must
51-
const remaining = BigInt(chunk.length) - written;
52-
let pendingAmt;
53-
if (remaining <= bytesAllowedRaw) {
54-
pendingAmt = chunk.length;
55-
} else if (remaining > bytesAllowedRaw) {
56-
pendingAmt = bytesAllowedRaw;
57-
}
58-
59-
// Write a view of the chunk in
60-
const view = new Uint8Array(chunk, Number(written), pendingAmt);
61-
outputStream.write(view);
62-
63-
written += BigInt(pendingAmt);
64-
}
65-
}
66-
67-
// Clean up pollable & stream
68-
pollable[Symbol.dispose]();
69-
outputStream[Symbol.dispose]();
70-
}
71-
72-
// Set the outgoing response body w/ no trailers
73-
OutgoingBody.finish(outgoingBody, undefined);
74-
75-
// Set the response outparam
76-
ResponseOutparam.set(outgoingWasiResp, {
77-
tag: 'ok',
78-
val: outgoingResponse,
6+
const f = genWriteWebResponseFn({
7+
types: {
8+
OutgoingResponse,
9+
Fields,
10+
OutgoingBody,
11+
ResponseOutparam,
12+
},
7913
});
14+
await f(resp, outgoingWasiResp);
8015
}
Lines changed: 5 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
/**
2-
* This file implements helpers and relevant types for use with `wasi:http` 0.2.x APIs
2+
* This file implements helpers and relevant types for use with `wasi:http@0.2.6`
33
*
44
* @see: https://github.com/WebAssembly/wasi-http
55
*/
66

7-
import { IncomingBody, IncomingRequest } from "wasi:http/types@0.2.6";
8-
import { Pollable } from "wasi:io/poll@0.2.6";
9-
import { InputStream } from "wasi:io/streams@0.2.6";
7+
import { type IncomingRequest, IncomingBody } from "wasi:http/types@0.2.6";
108

11-
import { ensureGlobalReadableStream, ensureGlobalRequest } from "../../../globals.js";
12-
import { wasiHTTPMethodToString, requestShouldHaveBody } from "../../../0.2.x/http/index.js";
13-
import { DEFAULT_INCOMING_BODY_READ_MAX_BYTES } from "../../../constants.js";
9+
import { genReadWASIRequestFn } from "../../../0.2.x/http/types/request.js";
1410

1511
/**
1612
* Create a web-platform `Request` from a `wasi:http/incoming-handler` `incoming-request`.
@@ -24,87 +20,6 @@ import { DEFAULT_INCOMING_BODY_READ_MAX_BYTES } from "../../../constants.js";
2420
* @see https://github.com/WebAssembly/wasi-http
2521
*/
2622
export async function readWASIRequest(wasiIncomingRequest: IncomingRequest): Promise<Request> {
27-
if (!wasiIncomingRequest) {
28-
throw new TypeError('WASI incoming request not provided');
29-
}
30-
const method = wasiHTTPMethodToString(wasiIncomingRequest.method());
31-
const pathWithQuery = wasiIncomingRequest.pathWithQuery();
32-
33-
const schemeRaw = wasiIncomingRequest.scheme();
34-
let scheme;
35-
switch (schemeRaw.tag) {
36-
case 'HTTP':
37-
scheme = 'http'
38-
break;
39-
case 'HTTPS':
40-
scheme = 'https'
41-
break;
42-
default:
43-
throw new Error(`unexpected scheme [${schemeRaw.tag}]`);
44-
}
45-
46-
const authority = wasiIncomingRequest.authority();
47-
const decoder = new TextDecoder('utf-8');
48-
const headers = Object.fromEntries(
49-
wasiIncomingRequest.headers().entries().map(([k,valueBytes]) => {
50-
return [k, decoder.decode(valueBytes)];
51-
})
52-
);
53-
const Request = ensureGlobalRequest();
54-
const ReadableStream = ensureGlobalReadableStream();
55-
56-
let incomingBody: IncomingBody;
57-
let incomingBodyStream: InputStream;
58-
let incomingBodyPollable: Pollable;
59-
60-
let body: ReadableStream;
61-
if (requestShouldHaveBody({ method })) {
62-
body = new ReadableStream({
63-
start(controller) {
64-
if (!incomingBody) {
65-
incomingBody = wasiIncomingRequest.consume();
66-
incomingBodyStream = incomingBody.stream();
67-
incomingBodyPollable = incomingBodyStream.subscribe();
68-
}
69-
},
70-
71-
pull(controller) {
72-
// Read all information coming from the request
73-
while (true) {
74-
// Wait until the pollable is ready
75-
if (!incomingBodyPollable.ready()) {
76-
incomingBodyPollable.block();
77-
}
78-
79-
try {
80-
const bytes = incomingBodyStream.read(DEFAULT_INCOMING_BODY_READ_MAX_BYTES);
81-
if (bytes.length === 0) {
82-
break;
83-
}
84-
controller.enqueue(bytes);
85-
} catch (err) {
86-
if (err.payload.tag === 'closed') { break; }
87-
throw err;
88-
}
89-
}
90-
91-
// Once information has all been read we can clean up
92-
incomingBodyPollable[Symbol.dispose]();
93-
incomingBodyStream[Symbol.dispose]();
94-
IncomingBody.finish(incomingBody);
95-
wasiIncomingRequest[Symbol.dispose]();
96-
controller.close();
97-
},
98-
});
99-
100-
}
101-
102-
const url = `${scheme}://${authority}${pathWithQuery}`;
103-
const req = new Request(url, {
104-
method,
105-
headers,
106-
body,
107-
});
108-
109-
return req;
23+
const f = genReadWASIRequestFn(IncomingBody)
24+
return await f(wasiIncomingRequest);
11025
}
Lines changed: 10 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,15 @@
1-
import { OutgoingBody, ResponseOutparam, Fields, OutgoingResponse, FieldValue } from 'wasi:http/types@0.2.6';
2-
3-
const ENCODER = new TextEncoder();
1+
import { OutgoingBody, ResponseOutparam, Fields, OutgoingResponse } from 'wasi:http/types@0.2.6';
2+
import { genWriteWebResponseFn } from '../../../0.2.x/http/types/response.js';
43

54
/** Write a Web `Response` into a `wasi:http@0.2.x#outgoing-response` */
65
export async function writeWebResponse(resp: Response, outgoingWasiResp: ResponseOutparam) {
7-
// Build headers
8-
const fields: [string, FieldValue][] = [];
9-
for (const [k,v] of [...resp.headers.entries()]) {
10-
fields.push([k.toString(), ENCODER.encode(v)]);
11-
}
12-
const headers = Fields.fromList(fields);
13-
const outgoingResponse = new OutgoingResponse(headers);
14-
15-
// Set status
16-
const status = resp.status;
17-
outgoingResponse.setStatusCode(status);
18-
19-
// Build the outgoing response body
20-
const outgoingBody = outgoingResponse.body();
21-
{
22-
// Create a stream for the response body
23-
const outputStream = outgoingBody.write();
24-
if (resp.body === null) {
25-
throw new Error("unexpectedly missing resp.body");
26-
}
27-
const pollable = outputStream.subscribe();
28-
29-
// Create a reader for the body we'll be writing out
30-
const reader = await resp.body.getReader();
31-
32-
while (true) {
33-
const { value: chunk, done } = await reader.read();
34-
if (done) { break; }
35-
36-
if (chunk.length === 0) {
37-
continue;
38-
}
39-
40-
let written = 0n;
41-
while (written < chunk.length) {
42-
// Wait until output stream is ready
43-
if (!pollable.ready()) {
44-
pollable.block();
45-
}
46-
47-
// Find out how much we are allowed to write
48-
const bytesAllowedRaw = outputStream.checkWrite();
49-
50-
// If we can't write as much as we want, we must
51-
const remaining = BigInt(chunk.length) - written;
52-
let pendingAmt;
53-
if (remaining <= bytesAllowedRaw) {
54-
pendingAmt = chunk.length;
55-
} else if (remaining > bytesAllowedRaw) {
56-
pendingAmt = bytesAllowedRaw;
57-
}
58-
59-
// Write a view of the chunk in
60-
const view = new Uint8Array(chunk, Number(written), pendingAmt);
61-
outputStream.write(view);
62-
63-
written += BigInt(pendingAmt);
64-
}
65-
}
66-
67-
// Clean up pollable & stream
68-
pollable[Symbol.dispose]();
69-
outputStream[Symbol.dispose]();
70-
}
71-
72-
// Set the outgoing response body w/ no trailers
73-
OutgoingBody.finish(outgoingBody, undefined);
74-
75-
// Set the response outparam
76-
ResponseOutparam.set(outgoingWasiResp, {
77-
tag: 'ok',
78-
val: outgoingResponse,
6+
const f = genWriteWebResponseFn({
7+
types: {
8+
OutgoingResponse,
9+
Fields,
10+
OutgoingBody,
11+
ResponseOutparam,
12+
},
7913
});
14+
await f(resp, outgoingWasiResp);
8015
}

0 commit comments

Comments
 (0)