@bytecodealliance/preview2-shim 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,570 +1,618 @@
1
1
  import {
2
- INPUT_STREAM_DISPOSE,
2
+ FUTURE_DISPOSE,
3
+ FUTURE_SUBSCRIBE,
4
+ FUTURE_TAKE_VALUE,
3
5
  HTTP_CREATE_REQUEST,
6
+ HTTP_OUTGOING_BODY_DISPOSE,
4
7
  HTTP_OUTPUT_STREAM_FINISH,
8
+ HTTP_SERVER_CLEAR_OUTGOING_RESPONSE,
9
+ HTTP_SERVER_SET_OUTGOING_RESPONSE,
10
+ HTTP_SERVER_START,
11
+ HTTP_SERVER_STOP,
5
12
  OUTPUT_STREAM_CREATE,
6
- FUTURE_GET_VALUE_AND_DISPOSE,
7
- FUTURE_DISPOSE,
13
+ OUTPUT_STREAM_DISPOSE,
8
14
  } from "../io/calls.js";
9
15
  import {
10
- ioCall,
11
- pollableCreate,
16
+ earlyDispose,
12
17
  inputStreamCreate,
18
+ ioCall,
13
19
  outputStreamCreate,
14
- outputStreamId,
20
+ pollableCreate,
21
+ registerDispose,
22
+ registerIncomingHttpHandler,
15
23
  } from "../io/worker-io.js";
16
24
  import { validateHeaderName, validateHeaderValue } from "node:http";
17
-
18
25
  import { HTTP } from "../io/calls.js";
19
26
 
20
27
  const symbolDispose = Symbol.dispose || Symbol.for("dispose");
21
28
  export const _forbiddenHeaders = new Set(["connection", "keep-alive"]);
22
29
 
23
- /**
24
- * @typedef {import("../../types/interfaces/wasi-http-types").Method} Method
25
- * @typedef {import("../../types/interfaces/wasi-http-types").Scheme} Scheme
26
- * @typedef {import("../../types/interfaces/wasi-http-types").Error} HttpError
27
- */
28
-
29
- export class WasiHttp {
30
- requestCnt = 1;
31
- responseCnt = 1;
32
- fieldsCnt = 1;
33
- futureCnt = 1;
34
-
35
- constructor() {
36
- const http = this;
37
-
38
- class IncomingBody {
39
- #finished = false;
40
- #streamId = undefined;
41
- stream() {
42
- if (!this.#streamId) throw undefined;
43
- const streamId = this.#streamId;
44
- this.#streamId = undefined;
45
- return inputStreamCreate(HTTP, streamId);
46
- }
47
- static finish(incomingBody) {
48
- if (incomingBody.#finished)
49
- throw new Error("incoming body already finished");
50
- incomingBody.#finished = true;
51
- return futureTrailersCreate(new Fields([]), false);
52
- }
53
- [symbolDispose]() {
54
- if (!this.#finished) {
55
- ioCall(INPUT_STREAM_DISPOSE | HTTP, this.#streamId);
56
- this.#streamId = undefined;
57
- }
58
- }
59
- static _create(streamId) {
60
- const incomingBody = new IncomingBody();
61
- incomingBody.#streamId = streamId;
62
- return incomingBody;
63
- }
64
- }
65
- const incomingBodyCreate = IncomingBody._create;
66
- delete IncomingBody._create;
67
-
68
- class IncomingRequest {
69
- #method;
70
- #pathWithQuery;
71
- #scheme;
72
- #authority;
73
- #headers;
74
- #streamId;
75
- method () {
76
- return this.#method;
77
- }
78
- pathWithQuery () {
79
- return this.#pathWithQuery;
80
- }
81
- scheme () {
82
- return this.#scheme;
83
- }
84
- authority () {
85
- return this.#authority;
86
- }
87
- headers () {
88
- return this.#headers;
89
- }
90
- consume () {
91
- return incomingBodyCreate(this.#streamId);
92
- }
93
- static _create(method, pathWithQuery, scheme, authority, streamId) {
94
- const incomingRequest = new IncomingRequest();
95
- incomingRequest.#method = method;
96
- incomingRequest.#pathWithQuery = pathWithQuery;
97
- incomingRequest.#scheme = scheme;
98
- incomingRequest.#authority = authority;
99
- incomingRequest.#streamId = streamId;
100
- return incomingRequest;
101
- }
102
- }
103
- const incomingRequestCreate = IncomingRequest._create;
104
- delete IncomingRequest._create;
105
-
106
- class FutureTrailers {
107
- _id = http.futureCnt++;
108
- #value;
109
- #isError;
110
- subscribe() {
111
- // TODO
112
- }
113
- get() {
114
- return { tag: this.#isError ? "err" : "ok", val: this.#value };
115
- }
116
- static _create(value, isError) {
117
- const res = new FutureTrailers();
118
- res.#value = value;
119
- res.#isError = isError;
120
- return res;
121
- }
122
- }
123
- const futureTrailersCreate = FutureTrailers._create;
124
- delete FutureTrailers._create;
125
-
126
- class OutgoingResponse {
127
- _id = http.responseCnt++;
128
- /** @type {number} */ #statusCode = 200;
129
- /** @type {Fields} */ #headers;
130
-
131
- /**
132
- * @param {number} statusCode
133
- * @param {Fields} headers
134
- */
135
- constructor(headers) {
136
- fieldsLock(headers);
137
- this.#headers = headers;
138
- }
30
+ class IncomingBody {
31
+ #finished = false;
32
+ #stream = undefined;
33
+ stream() {
34
+ if (!this.#stream) throw undefined;
35
+ const stream = this.#stream;
36
+ this.#stream = null;
37
+ return stream;
38
+ }
39
+ static finish(incomingBody) {
40
+ if (incomingBody.#finished)
41
+ throw new Error("incoming body already finished");
42
+ incomingBody.#finished = true;
43
+ return futureTrailersCreate();
44
+ }
45
+ [symbolDispose]() {}
46
+ static _create(streamId) {
47
+ const incomingBody = new IncomingBody();
48
+ incomingBody.#stream = inputStreamCreate(HTTP, streamId);
49
+ return incomingBody;
50
+ }
51
+ }
52
+ const incomingBodyCreate = IncomingBody._create;
53
+ delete IncomingBody._create;
54
+
55
+ class IncomingRequest {
56
+ #method;
57
+ #pathWithQuery;
58
+ #scheme;
59
+ #authority;
60
+ #headers;
61
+ #streamId;
62
+ method() {
63
+ return this.#method;
64
+ }
65
+ pathWithQuery() {
66
+ return this.#pathWithQuery;
67
+ }
68
+ scheme() {
69
+ return this.#scheme;
70
+ }
71
+ authority() {
72
+ return this.#authority;
73
+ }
74
+ headers() {
75
+ return this.#headers;
76
+ }
77
+ consume() {
78
+ return incomingBodyCreate(this.#streamId);
79
+ }
80
+ [symbolDispose]() {}
81
+ static _create(method, pathWithQuery, scheme, authority, headers, streamId) {
82
+ const incomingRequest = new IncomingRequest();
83
+ incomingRequest.#method = method;
84
+ incomingRequest.#pathWithQuery = pathWithQuery;
85
+ incomingRequest.#scheme = scheme;
86
+ incomingRequest.#authority = authority;
87
+ incomingRequest.#headers = headers;
88
+ incomingRequest.#streamId = streamId;
89
+ return incomingRequest;
90
+ }
91
+ }
92
+ const incomingRequestCreate = IncomingRequest._create;
93
+ delete IncomingRequest._create;
139
94
 
140
- statusCode() {
141
- return this.#statusCode;
142
- }
95
+ class FutureTrailers {
96
+ #requested = false;
97
+ subscribe() {
98
+ return pollableCreate(0, this);
99
+ }
100
+ get() {
101
+ if (this.#requested) return { tag: "err" };
102
+ this.#requested = true;
103
+ return {
104
+ tag: "ok",
105
+ val: {
106
+ tag: "ok",
107
+ val: undefined,
108
+ },
109
+ };
110
+ }
111
+ static _create() {
112
+ const res = new FutureTrailers();
113
+ return res;
114
+ }
115
+ }
116
+ const futureTrailersCreate = FutureTrailers._create;
117
+ delete FutureTrailers._create;
118
+
119
+ class OutgoingResponse {
120
+ #body;
121
+ /** @type {number} */ #statusCode = 200;
122
+ /** @type {Fields} */ #headers;
123
+
124
+ /**
125
+ * @param {number} statusCode
126
+ * @param {Fields} headers
127
+ */
128
+ constructor(headers) {
129
+ fieldsLock(headers);
130
+ this.#headers = headers;
131
+ }
143
132
 
144
- setStatusCode(statusCode) {
145
- this.#statusCode = statusCode;
146
- }
133
+ statusCode() {
134
+ return this.#statusCode;
135
+ }
147
136
 
148
- headers() {
149
- return this.#headers;
150
- }
137
+ setStatusCode(statusCode) {
138
+ this.#statusCode = statusCode;
139
+ }
151
140
 
152
- body() {
153
- let contentLengthValues = this.#headers.get("content-length");
154
- if (contentLengthValues.length === 0)
155
- contentLengthValues = this.#headers.get("Content-Length");
156
- let contentLength;
157
- if (contentLengthValues.length > 0)
158
- contentLength = Number(
159
- new TextDecoder().decode(contentLengthValues[0])
160
- );
161
- return outgoingBodyCreate(contentLength);
162
- }
163
- }
141
+ headers() {
142
+ return this.#headers;
143
+ }
164
144
 
165
- class ResponseOutparam {
166
- #response;
167
- static set(param, response) {
168
- param.#response = response;
169
- }
170
- }
145
+ body() {
146
+ let contentLengthValues = this.#headers.get("content-length");
147
+ let contentLength;
148
+ if (contentLengthValues.length > 0)
149
+ contentLength = Number(new TextDecoder().decode(contentLengthValues[0]));
150
+ this.#body = outgoingBodyCreate(contentLength);
151
+ return this.#body;
152
+ }
171
153
 
172
- class RequestOptions {
173
- #connectTimeoutMs;
174
- #firstByteTimeoutMs;
175
- #betweenBytesTimeoutMs;
176
- connectTimeoutMs () {
177
- return this.#connectTimeoutMs;
178
- }
179
- setConnectTimeoutMs (duration) {
180
- this.#connectTimeoutMs = duration;
181
- }
182
- firstByteTimeoutMs () {
183
- return this.#firstByteTimeoutMs;
184
- }
185
- setFirstByteTimeoutMs (duration) {
186
- this.#firstByteTimeoutMs = duration;
187
- }
188
- betweenBytesTimeoutMs () {
189
- return this.#betweenBytesTimeoutMs;
190
- }
191
- setBetweenBytesTimeoutMs (duration) {
192
- this.#betweenBytesTimeoutMs = duration;
193
- }
154
+ static _body(outgoingResponse) {
155
+ return outgoingResponse.#body;
156
+ }
157
+ }
158
+
159
+ const outgoingResponseBody = OutgoingResponse._body;
160
+ delete OutgoingResponse._body;
161
+
162
+ class ResponseOutparam {
163
+ #setListener;
164
+ static set(param, response) {
165
+ param.#setListener(response);
166
+ }
167
+ static _create(setListener) {
168
+ const responseOutparam = new ResponseOutparam();
169
+ responseOutparam.#setListener = setListener;
170
+ return responseOutparam;
171
+ }
172
+ }
173
+ const responseOutparamCreate = ResponseOutparam._create;
174
+ delete ResponseOutparam._create;
175
+
176
+ class RequestOptions {
177
+ #connectTimeoutMs;
178
+ #firstByteTimeoutMs;
179
+ #betweenBytesTimeoutMs;
180
+ connectTimeoutMs() {
181
+ return this.#connectTimeoutMs;
182
+ }
183
+ setConnectTimeoutMs(duration) {
184
+ this.#connectTimeoutMs = duration;
185
+ }
186
+ firstByteTimeoutMs() {
187
+ return this.#firstByteTimeoutMs;
188
+ }
189
+ setFirstByteTimeoutMs(duration) {
190
+ this.#firstByteTimeoutMs = duration;
191
+ }
192
+ betweenBytesTimeoutMs() {
193
+ return this.#betweenBytesTimeoutMs;
194
+ }
195
+ setBetweenBytesTimeoutMs(duration) {
196
+ this.#betweenBytesTimeoutMs = duration;
197
+ }
198
+ }
199
+
200
+ class OutgoingRequest {
201
+ /** @type {Method} */ #method = { tag: "get" };
202
+ /** @type {Scheme | undefined} */ #scheme = undefined;
203
+ /** @type {string | undefined} */ #pathWithQuery = undefined;
204
+ /** @type {string | undefined} */ #authority = undefined;
205
+ /** @type {Fields} */ #headers;
206
+ /** @type {OutgoingBody} */ #body;
207
+ #bodyRequested = false;
208
+ constructor(headers) {
209
+ fieldsLock(headers);
210
+ this.#headers = headers;
211
+ let contentLengthValues = this.#headers.get("content-length");
212
+ if (contentLengthValues.length === 0)
213
+ contentLengthValues = this.#headers.get("Content-Length");
214
+ let contentLength;
215
+ if (contentLengthValues.length > 0)
216
+ contentLength = Number(new TextDecoder().decode(contentLengthValues[0]));
217
+ this.#body = outgoingBodyCreate(contentLength);
218
+ }
219
+ body() {
220
+ if (this.#bodyRequested) throw new Error("Body already requested");
221
+ this.#bodyRequested = true;
222
+ return this.#body;
223
+ }
224
+ method() {
225
+ return this.#method;
226
+ }
227
+ setMethod(method) {
228
+ if (method.tag === "other" && !method.val.match(/^[a-zA-Z-]+$/))
229
+ throw undefined;
230
+ this.#method = method;
231
+ }
232
+ pathWithQuery() {
233
+ return this.#pathWithQuery;
234
+ }
235
+ setPathWithQuery(pathWithQuery) {
236
+ if (
237
+ pathWithQuery &&
238
+ !pathWithQuery.match(/^[a-zA-Z0-9.\-_~!$&'()*+,;=:@%?/]+$/)
239
+ )
240
+ throw undefined;
241
+ this.#pathWithQuery = pathWithQuery;
242
+ }
243
+ scheme() {
244
+ return this.#scheme;
245
+ }
246
+ setScheme(scheme) {
247
+ if (scheme?.tag === "other" && !scheme.val.match(/^[a-zA-Z]+$/))
248
+ throw undefined;
249
+ this.#scheme = scheme;
250
+ }
251
+ authority() {
252
+ return this.#authority;
253
+ }
254
+ setAuthority(authority) {
255
+ if (authority) {
256
+ const [host, port, ...extra] = authority.split(":");
257
+ const portNum = Number(port);
258
+ if (
259
+ extra.length ||
260
+ (port !== undefined &&
261
+ (portNum.toString() !== port || portNum > 9999)) ||
262
+ !host.match(/^[a-zA-Z0-9-.]+$/)
263
+ )
264
+ throw undefined;
265
+ }
266
+ this.#authority = authority;
267
+ }
268
+ headers() {
269
+ return this.#headers;
270
+ }
271
+ [symbolDispose]() {}
272
+ static _handle(request, options) {
273
+ const connectTimeout = options?.connectTimeoutMs();
274
+ const betweenBytesTimeout = options?.betweenBytesTimeoutMs();
275
+ const firstByteTimeout = options?.firstByteTimeoutMs();
276
+ const scheme = schemeString(request.#scheme);
277
+ // note: host header is automatically added by Node.js
278
+ const headers = [];
279
+ const decoder = new TextDecoder();
280
+ for (const [key, value] of request.#headers.entries()) {
281
+ headers.push([key, decoder.decode(value)]);
194
282
  }
283
+ return futureIncomingResponseCreate(
284
+ request.#method.val || request.#method.tag,
285
+ scheme,
286
+ request.#authority,
287
+ request.#pathWithQuery,
288
+ headers,
289
+ outgoingBodyOutputStreamId(request.#body),
290
+ connectTimeout,
291
+ betweenBytesTimeout,
292
+ firstByteTimeout
293
+ );
294
+ }
295
+ }
195
296
 
196
- class OutgoingRequest {
197
- _id = http.requestCnt++;
198
- /** @type {Method} */ #method = { tag: "get" };
199
- /** @type {Scheme | undefined} */ #scheme = undefined;
200
- /** @type {string | undefined} */ #pathWithQuery = undefined;
201
- /** @type {string | undefined} */ #authority = undefined;
202
- /** @type {Fields} */ #headers;
203
- /** @type {OutgoingBody} */ #body;
204
- #bodyRequested = false;
205
- constructor(headers) {
206
- fieldsLock(headers);
207
- this.#headers = headers;
208
- let contentLengthValues = this.#headers.get("content-length");
209
- if (contentLengthValues.length === 0)
210
- contentLengthValues = this.#headers.get("Content-Length");
211
- let contentLength;
212
- if (contentLengthValues.length > 0)
213
- contentLength = Number(
214
- new TextDecoder().decode(contentLengthValues[0])
215
- );
216
- this.#body = outgoingBodyCreate(contentLength);
217
- }
218
- body() {
219
- if (this.#bodyRequested) throw new Error("Body already requested");
220
- this.#bodyRequested = true;
221
- return this.#body;
222
- }
223
- method() {
224
- return this.#method;
225
- }
226
- setMethod(method) {
227
- if (method.tag === "other" && !method.val.match(/^[a-zA-Z-]+$/))
228
- throw undefined;
229
- this.#method = method;
230
- }
231
- pathWithQuery() {
232
- return this.#pathWithQuery;
233
- }
234
- setPathWithQuery(pathWithQuery) {
235
- if (pathWithQuery && !pathWithQuery.match(/^[a-zA-Z0-9.-_~!$&'()*+,;=:@%/]+$/))
236
- throw undefined;
237
- this.#pathWithQuery = pathWithQuery;
238
- }
239
- scheme() {
240
- return this.#scheme;
241
- }
242
- setScheme(scheme) {
243
- if (scheme?.tag === "other" && !scheme.val.match(/^[a-zA-Z]+$/))
244
- throw undefined;
245
- this.#scheme = scheme;
246
- }
247
- authority() {
248
- return this.#authority;
249
- }
250
- setAuthority(authority) {
251
- if (authority) {
252
- const [host, port, ...extra] = authority.split(':');
253
- const portNum = Number(port);
254
- if (extra.length || port !== undefined && (portNum.toString() !== port || portNum > 9999) || !host.match(/^[a-zA-Z0-9-.]+$/))
255
- throw undefined;
256
- }
257
- this.#authority = authority;
258
- }
259
- headers() {
260
- return this.#headers;
261
- }
262
- [symbolDispose]() {}
263
- static _handle(request, _options) {
264
- // TODO: handle options timeouts
265
- const scheme = schemeString(request.#scheme);
266
- const url = scheme + request.#authority + (request.#pathWithQuery || '');
267
- const headers = [["host", request.#authority]];
268
- const decoder = new TextDecoder();
269
- for (const [key, value] of request.#headers.entries()) {
270
- headers.push([key, decoder.decode(value)]);
271
- }
272
- return futureIncomingResponseCreate(
273
- request.#method.val || request.#method.tag,
274
- url,
275
- Object.entries(headers),
276
- outgoingBodyOutputStreamId(request.#body)
277
- );
278
- }
297
+ const outgoingRequestHandle = OutgoingRequest._handle;
298
+ delete OutgoingRequest._handle;
299
+
300
+ class OutgoingBody {
301
+ #outputStream = null;
302
+ #outputStreamId = null;
303
+ #contentLength = undefined;
304
+ #finalizer;
305
+ write() {
306
+ // can only call write once
307
+ const outputStream = this.#outputStream;
308
+ if (outputStream === null) throw undefined;
309
+ this.#outputStream = null;
310
+ return outputStream;
311
+ }
312
+ /**
313
+ * @param {OutgoingBody} body
314
+ * @param {Fields | undefined} trailers
315
+ */
316
+ static finish(body, trailers) {
317
+ if (trailers) throw { tag: "internal-error", val: "trailers unsupported" };
318
+ // this will verify content length, and also verify not already finished
319
+ // throwing errors as appropriate
320
+ ioCall(HTTP_OUTPUT_STREAM_FINISH, body.#outputStreamId, null);
321
+ }
322
+ static _outputStreamId(outgoingBody) {
323
+ return outgoingBody.#outputStreamId;
324
+ }
325
+ static _create(contentLength) {
326
+ const outgoingBody = new OutgoingBody();
327
+ outgoingBody.#contentLength = contentLength;
328
+ outgoingBody.#outputStreamId = ioCall(
329
+ OUTPUT_STREAM_CREATE | HTTP,
330
+ null,
331
+ outgoingBody.#contentLength
332
+ );
333
+ outgoingBody.#outputStream = outputStreamCreate(
334
+ HTTP,
335
+ outgoingBody.#outputStreamId
336
+ );
337
+ outgoingBody.#finalizer = registerDispose(
338
+ outgoingBody,
339
+ null,
340
+ outgoingBody.#outputStreamId,
341
+ outgoingBodyDispose
342
+ );
343
+ return outgoingBody;
344
+ }
345
+ [symbolDispose]() {
346
+ if (this.#finalizer) {
347
+ earlyDispose(this.#finalizer);
348
+ this.#finalizer = null;
279
349
  }
350
+ }
351
+ }
280
352
 
281
- const outgoingRequestHandle = OutgoingRequest._handle;
282
- delete OutgoingRequest._handle;
283
-
284
- class OutgoingBody {
285
- #outputStream = undefined;
286
- #contentLength = undefined;
287
- write() {
288
- if (this.#outputStream)
289
- throw new Error("output stream already created for writing");
290
- this.#outputStream = outputStreamCreate(
291
- HTTP,
292
- ioCall(OUTPUT_STREAM_CREATE | HTTP, null, this.#contentLength)
293
- );
294
- this.#outputStream[symbolDispose] = () => {};
295
- return this.#outputStream;
296
- }
297
- [symbolDispose]() {
298
- this.#outputStream?.[symbolDispose]();
299
- }
300
- /**
301
- * @param {OutgoingBody} body
302
- * @param {Fields | undefined} trailers
303
- */
304
- static finish(body, _trailers) {
305
- // this will verify content length, and also verify not already finished
306
- // throwing errors as appropriate
307
- if (body.#outputStream)
308
- ioCall(
309
- HTTP_OUTPUT_STREAM_FINISH,
310
- outputStreamId(body.#outputStream),
311
- null
312
- );
313
- body.#outputStream?.[symbolDispose]();
314
- }
315
- static _outputStreamId(outgoingBody) {
316
- if (outgoingBody.#outputStream)
317
- return outputStreamId(outgoingBody.#outputStream);
318
- }
319
- static _create(contentLength) {
320
- const outgoingBody = new OutgoingBody();
321
- outgoingBody.#contentLength = contentLength;
322
- return outgoingBody;
323
- }
353
+ function outgoingBodyDispose(id) {
354
+ ioCall(HTTP_OUTGOING_BODY_DISPOSE, id, null);
355
+ }
356
+
357
+ const outgoingBodyOutputStreamId = OutgoingBody._outputStreamId;
358
+ delete OutgoingBody._outputStreamId;
359
+
360
+ const outgoingBodyCreate = OutgoingBody._create;
361
+ delete OutgoingBody._create;
362
+
363
+ class IncomingResponse {
364
+ /** @type {Fields} */ #headers = undefined;
365
+ #status = 0;
366
+ /** @type {number} */ #bodyStream;
367
+ status() {
368
+ return this.#status;
369
+ }
370
+ headers() {
371
+ return this.#headers;
372
+ }
373
+ consume() {
374
+ if (this.#bodyStream === undefined) throw undefined;
375
+ const bodyStream = this.#bodyStream;
376
+ this.#bodyStream = undefined;
377
+ return bodyStream;
378
+ }
379
+ [symbolDispose]() {
380
+ if (this.#bodyStream) this.#bodyStream[symbolDispose]();
381
+ }
382
+ static _create(status, headers, bodyStreamId) {
383
+ const res = new IncomingResponse();
384
+ res.#status = status;
385
+ res.#headers = headers;
386
+ res.#bodyStream = incomingBodyCreate(bodyStreamId);
387
+ return res;
388
+ }
389
+ }
390
+
391
+ const incomingResponseCreate = IncomingResponse._create;
392
+ delete IncomingResponse._create;
393
+
394
+ class FutureIncomingResponse {
395
+ #id;
396
+ #finalizer;
397
+ subscribe() {
398
+ return pollableCreate(
399
+ ioCall(FUTURE_SUBSCRIBE | HTTP, this.#id, null),
400
+ this
401
+ );
402
+ }
403
+ get() {
404
+ const ret = ioCall(FUTURE_TAKE_VALUE | HTTP, this.#id, null);
405
+ if (ret === undefined) return undefined;
406
+ if (ret.tag === "ok" && ret.val.tag === "ok") {
407
+ const textEncoder = new TextEncoder();
408
+ const { status, headers, bodyStreamId } = ret.val.val;
409
+ ret.val.val = incomingResponseCreate(
410
+ status,
411
+ fieldsFromEntriesChecked(
412
+ headers.map(([key, val]) => [key, textEncoder.encode(val)])
413
+ ),
414
+ bodyStreamId
415
+ );
324
416
  }
325
- const outgoingBodyOutputStreamId = OutgoingBody._outputStreamId;
326
- delete OutgoingBody._outputStreamId;
327
-
328
- const outgoingBodyCreate = OutgoingBody._create;
329
- delete OutgoingBody._create;
330
-
331
- class IncomingResponse {
332
- _id = http.responseCnt++;
333
- /** @type {Fields} */ #headers = undefined;
334
- #status = 0;
335
- /** @type {number} */ #bodyStreamId;
336
- status() {
337
- return this.#status;
338
- }
339
- headers() {
340
- return this.#headers;
341
- }
342
- consume() {
343
- if (this.#bodyStreamId === undefined) throw undefined;
344
- const bodyStreamId = this.#bodyStreamId;
345
- this.#bodyStreamId = undefined;
346
- return incomingBodyCreate(bodyStreamId);
347
- }
348
- [symbolDispose]() {
349
- if (this.#bodyStreamId) {
350
- ioCall(INPUT_STREAM_DISPOSE | HTTP, this.#bodyStreamId);
351
- this.#bodyStreamId = undefined;
352
- }
353
- }
354
- static _create(status, headers, bodyStreamId) {
355
- const res = new IncomingResponse();
356
- res.#status = status;
357
- res.#headers = headers;
358
- res.#bodyStreamId = bodyStreamId;
359
- return res;
360
- }
417
+ return ret;
418
+ }
419
+ static _create(
420
+ method,
421
+ scheme,
422
+ authority,
423
+ pathWithQuery,
424
+ headers,
425
+ body,
426
+ connectTimeout,
427
+ betweenBytesTimeout,
428
+ firstByteTimeout
429
+ ) {
430
+ const res = new FutureIncomingResponse();
431
+ res.#id = ioCall(HTTP_CREATE_REQUEST, null, {
432
+ method,
433
+ scheme,
434
+ authority,
435
+ pathWithQuery,
436
+ headers,
437
+ body,
438
+ connectTimeout,
439
+ betweenBytesTimeout,
440
+ firstByteTimeout,
441
+ });
442
+ res.#finalizer = registerDispose(
443
+ res,
444
+ null,
445
+ res.#id,
446
+ futureIncomingResponseDispose
447
+ );
448
+ return res;
449
+ }
450
+ [symbolDispose]() {
451
+ if (this.#finalizer) {
452
+ earlyDispose(this.#finalizer);
453
+ this.#finalizer = null;
361
454
  }
455
+ }
456
+ }
362
457
 
363
- const incomingResponseCreate = IncomingResponse._create;
364
- delete IncomingResponse._create;
458
+ function futureIncomingResponseDispose(id) {
459
+ ioCall(FUTURE_DISPOSE | HTTP, id, null);
460
+ }
365
461
 
366
- class FutureIncomingResponse {
367
- _id = http.futureCnt++;
368
- #pollId;
369
- subscribe() {
370
- if (this.#pollId) return pollableCreate(this.#pollId);
371
- // 0 poll is immediately resolving
372
- return pollableCreate(0);
373
- }
374
- get() {
375
- // already taken
376
- if (!this.#pollId) return { tag: "err" };
377
- const ret = ioCall(FUTURE_GET_VALUE_AND_DISPOSE | HTTP, this.#pollId);
378
- if (!ret) return;
379
- this.#pollId = undefined;
380
- if (ret.error)
381
- return { tag: "ok", val: { tag: "err", val: ret.value } };
382
- const { status, headers, bodyStreamId } = ret.value;
383
- const textEncoder = new TextEncoder();
384
- return {
385
- tag: "ok",
386
- val: {
387
- tag: "ok",
388
- val: incomingResponseCreate(
389
- status,
390
- fieldsFromEntriesSafe(
391
- headers.map(([key, val]) => [key, textEncoder.encode(val)])
392
- ),
393
- bodyStreamId
394
- ),
395
- },
396
- };
397
- }
398
- [symbolDispose]() {
399
- if (this.#pollId) ioCall(FUTURE_DISPOSE | HTTP, this.#pollId);
400
- }
401
- static _create(method, url, headers, body) {
402
- const res = new FutureIncomingResponse();
403
- res.#pollId = ioCall(HTTP_CREATE_REQUEST, null, {
404
- method,
405
- url,
406
- headers,
407
- body,
408
- });
409
- return res;
410
- }
462
+ const futureIncomingResponseCreate = FutureIncomingResponse._create;
463
+ delete FutureIncomingResponse._create;
464
+
465
+ class Fields {
466
+ #immutable = false;
467
+ /** @type {[string, Uint8Array[]][]} */ #entries = [];
468
+ /** @type {Map<string, [string, Uint8Array[]][]>} */ #table = new Map();
469
+
470
+ /**
471
+ * @param {[string, Uint8Array[][]][]} entries
472
+ */
473
+ static fromList(entries) {
474
+ const fields = new Fields();
475
+ for (const [key, value] of entries) {
476
+ fields.append(key, value);
411
477
  }
412
-
413
- const futureIncomingResponseCreate = FutureIncomingResponse._create;
414
- delete FutureIncomingResponse._create;
415
-
416
- class Fields {
417
- _id = http.fieldsCnt++;
418
- #immutable = false;
419
- /** @type {[string, Uint8Array[]][]} */ #entries = [];
420
- /** @type {Map<string, [string, Uint8Array[]][]>} */ #table = new Map();
421
-
422
- /**
423
- * @param {[string, Uint8Array[][]][]} entries
424
- */
425
- static fromList(entries) {
426
- const fields = new Fields();
427
- for (const [key, value] of entries) {
428
- fields.append(key, value);
429
- }
430
- return fields;
431
- }
432
- get(name) {
433
- const tableEntries = this.#table.get(name.toLowerCase());
434
- if (!tableEntries) return [];
435
- return tableEntries.map(([, v]) => v);
436
- }
437
- set(name, values) {
438
- if (this.#immutable) throw { tag: "immutable" };
439
- try {
440
- validateHeaderName(name);
441
- } catch {
442
- throw { tag: "invalid-syntax" };
443
- }
444
- for (const value of values) {
445
- try {
446
- validateHeaderValue(name, new TextDecoder().decode(value));
447
- } catch {
448
- throw { tag: "invalid-syntax" };
449
- }
450
- throw { tag: "invalid-syntax" };
451
- }
452
- const lowercased = name.toLowerCase();
453
- if (_forbiddenHeaders.has(lowercased)) throw { tag: "forbidden" };
454
- const tableEntries = this.#table.get(lowercased);
455
- if (tableEntries)
456
- this.#entries = this.#entries.filter(
457
- (entry) => !tableEntries.includes(entry)
458
- );
459
- tableEntries.splice(0, tableEntries.length);
460
- for (const value of values) {
461
- const entry = [name, value];
462
- this.#entries.push(entry);
463
- tableEntries.push(entry);
464
- }
465
- }
466
- delete(name) {
467
- if (this.#immutable) throw { tag: "immutable" };
468
- const lowercased = name.toLowerCase();
469
- const tableEntries = this.#table.get(lowercased);
470
- if (tableEntries) {
471
- this.#entries = this.#entries.filter(
472
- (entry) => !tableEntries.includes(entry)
473
- );
474
- this.#table.delete(lowercased);
475
- }
476
- }
477
- append(name, value) {
478
- if (this.#immutable) throw { tag: "immutable" };
479
- try {
480
- validateHeaderName(name);
481
- } catch {
482
- throw { tag: "invalid-syntax" };
483
- }
484
- try {
485
- validateHeaderValue(name, new TextDecoder().decode(value));
486
- } catch (e) {
487
- throw { tag: "invalid-syntax" };
488
- }
489
- const lowercased = name.toLowerCase();
490
- if (_forbiddenHeaders.has(lowercased)) throw { tag: "forbidden" };
491
- const entry = [name, value];
492
- this.#entries.push(entry);
493
- const tableEntries = this.#table.get(lowercased);
494
- if (tableEntries) {
495
- tableEntries.push(entry);
496
- } else {
497
- this.#table.set(lowercased, [entry]);
498
- }
499
- }
500
- entries() {
501
- return this.#entries;
502
- }
503
- clone() {
504
- return fieldsFromEntriesSafe(this.#entries);
505
- }
506
- static _lock(fields) {
507
- fields.#immutable = true;
508
- }
509
- // assumes entries are already validated
510
- static _fromEntriesChecked(entries) {
511
- const fields = new Fields();
512
- fields.#entries = entries;
513
- for (const entry of entries) {
514
- const lowercase = entry[0].toLowerCase();
515
- const existing = fields.#table.get(lowercase);
516
- if (existing) {
517
- existing.push(entry);
518
- } else {
519
- fields.#table.set(lowercase, [entry]);
520
- }
521
- }
522
- return fields;
478
+ return fields;
479
+ }
480
+ get(name) {
481
+ const tableEntries = this.#table.get(name.toLowerCase());
482
+ if (!tableEntries) return [];
483
+ return tableEntries.map(([, v]) => v);
484
+ }
485
+ set(name, values) {
486
+ if (this.#immutable) throw { tag: "immutable" };
487
+ try {
488
+ validateHeaderName(name);
489
+ } catch {
490
+ throw { tag: "invalid-syntax" };
491
+ }
492
+ for (const value of values) {
493
+ try {
494
+ validateHeaderValue(name, new TextDecoder().decode(value));
495
+ } catch {
496
+ throw { tag: "invalid-syntax" };
523
497
  }
498
+ throw { tag: "invalid-syntax" };
524
499
  }
525
- const fieldsLock = Fields._lock;
526
- delete Fields._lock;
527
- const fieldsFromEntriesSafe = Fields._fromEntriesChecked;
528
- delete Fields._fromEntriesChecked;
529
-
530
- this.outgoingHandler = {
531
- /**
532
- * @param {OutgoingRequest} request
533
- * @param {RequestOptions | undefined} options
534
- * @returns {FutureIncomingResponse}
535
- */
536
- handle: outgoingRequestHandle,
537
- };
538
-
539
- this._incomingRequestCreate = incomingRequestCreate;
540
-
541
- function httpErrorCode(err) {
542
- return {
543
- tag: "internal-error",
544
- val: err.message,
545
- };
500
+ const lowercased = name.toLowerCase();
501
+ if (_forbiddenHeaders.has(lowercased)) throw { tag: "forbidden" };
502
+ const tableEntries = this.#table.get(lowercased);
503
+ if (tableEntries)
504
+ this.#entries = this.#entries.filter(
505
+ (entry) => !tableEntries.includes(entry)
506
+ );
507
+ tableEntries.splice(0, tableEntries.length);
508
+ for (const value of values) {
509
+ const entry = [name, value];
510
+ this.#entries.push(entry);
511
+ tableEntries.push(entry);
546
512
  }
547
-
548
- this.types = {
549
- Fields,
550
- FutureIncomingResponse,
551
- FutureTrailers,
552
- IncomingBody,
553
- IncomingRequest,
554
- IncomingResponse,
555
- OutgoingBody,
556
- OutgoingRequest,
557
- OutgoingResponse,
558
- ResponseOutparam,
559
- RequestOptions,
560
- httpErrorCode,
561
- };
562
513
  }
514
+ has(name) {
515
+ return this.#table.has(name.toLowerCase());
516
+ }
517
+ delete(name) {
518
+ if (this.#immutable) throw { tag: "immutable" };
519
+ const lowercased = name.toLowerCase();
520
+ const tableEntries = this.#table.get(lowercased);
521
+ if (tableEntries) {
522
+ this.#entries = this.#entries.filter(
523
+ (entry) => !tableEntries.includes(entry)
524
+ );
525
+ this.#table.delete(lowercased);
526
+ }
527
+ }
528
+ append(name, value) {
529
+ if (this.#immutable) throw { tag: "immutable" };
530
+ try {
531
+ validateHeaderName(name);
532
+ } catch {
533
+ throw { tag: "invalid-syntax" };
534
+ }
535
+ try {
536
+ validateHeaderValue(name, new TextDecoder().decode(value));
537
+ } catch (e) {
538
+ throw { tag: "invalid-syntax" };
539
+ }
540
+ const lowercased = name.toLowerCase();
541
+ if (_forbiddenHeaders.has(lowercased)) throw { tag: "forbidden" };
542
+ const entry = [name, value];
543
+ this.#entries.push(entry);
544
+ const tableEntries = this.#table.get(lowercased);
545
+ if (tableEntries) {
546
+ tableEntries.push(entry);
547
+ } else {
548
+ this.#table.set(lowercased, [entry]);
549
+ }
550
+ }
551
+ entries() {
552
+ return this.#entries;
553
+ }
554
+ clone() {
555
+ return fieldsFromEntriesChecked(this.#entries);
556
+ }
557
+ static _lock(fields) {
558
+ fields.#immutable = true;
559
+ return fields;
560
+ }
561
+ // assumes entries are already validated
562
+ static _fromEntriesChecked(entries) {
563
+ const fields = new Fields();
564
+ fields.#entries = entries;
565
+ for (const entry of entries) {
566
+ const lowercase = entry[0].toLowerCase();
567
+ const existing = fields.#table.get(lowercase);
568
+ if (existing) {
569
+ existing.push(entry);
570
+ } else {
571
+ fields.#table.set(lowercase, [entry]);
572
+ }
573
+ }
574
+ return fields;
575
+ }
576
+ }
577
+ const fieldsLock = Fields._lock;
578
+ delete Fields._lock;
579
+ const fieldsFromEntriesChecked = Fields._fromEntriesChecked;
580
+ delete Fields._fromEntriesChecked;
581
+
582
+ export const outgoingHandler = {
583
+ /**
584
+ * @param {OutgoingRequest} request
585
+ * @param {RequestOptions | undefined} options
586
+ * @returns {FutureIncomingResponse}
587
+ */
588
+ handle: outgoingRequestHandle,
589
+ };
590
+
591
+ function httpErrorCode(err) {
592
+ if (err.payload) return err.payload;
593
+ return {
594
+ tag: "internal-error",
595
+ val: err.message,
596
+ };
563
597
  }
564
598
 
599
+ export const types = {
600
+ Fields,
601
+ FutureIncomingResponse,
602
+ FutureTrailers,
603
+ IncomingBody,
604
+ IncomingRequest,
605
+ IncomingResponse,
606
+ OutgoingBody,
607
+ OutgoingRequest,
608
+ OutgoingResponse,
609
+ ResponseOutparam,
610
+ RequestOptions,
611
+ httpErrorCode,
612
+ };
613
+
565
614
  function schemeString(scheme) {
566
- if (!scheme)
567
- return 'https:';
615
+ if (!scheme) return "https:";
568
616
  switch (scheme.tag) {
569
617
  case "HTTP":
570
618
  return "http:";
@@ -575,4 +623,87 @@ function schemeString(scheme) {
575
623
  }
576
624
  }
577
625
 
578
- export const { outgoingHandler, types } = new WasiHttp();
626
+ const supportedMethods = [
627
+ "get",
628
+ "head",
629
+ "post",
630
+ "put",
631
+ "delete",
632
+ "connect",
633
+ "options",
634
+ "trace",
635
+ "patch",
636
+ ];
637
+ function parseMethod(method) {
638
+ const lowercase = method.toLowerCase();
639
+ if (supportedMethods.includes(method.toLowerCase()))
640
+ return { tag: lowercase };
641
+ return { tag: "other", val: lowercase };
642
+ }
643
+
644
+ const httpServers = new Map();
645
+ let httpServerCnt = 0;
646
+ export class HTTPServer {
647
+ #id = ++httpServerCnt;
648
+ #liveEventLoopInterval;
649
+ constructor(incomingHandler) {
650
+ httpServers.set(this.#id, this);
651
+ if (typeof incomingHandler?.handle !== "function") {
652
+ console.error("Not a valid HTTP server component to execute.");
653
+ process.exit(1);
654
+ }
655
+ registerIncomingHttpHandler(
656
+ this.#id,
657
+ ({ method, pathWithQuery, host, headers, responseId, streamId }) => {
658
+ const textEncoder = new TextEncoder();
659
+ const request = incomingRequestCreate(
660
+ parseMethod(method),
661
+ pathWithQuery,
662
+ { tag: "HTTP" },
663
+ host,
664
+ fieldsLock(
665
+ fieldsFromEntriesChecked(
666
+ headers
667
+ .filter(([key]) => !_forbiddenHeaders.has(key))
668
+ .map(([key, val]) => [key, textEncoder.encode(val)])
669
+ )
670
+ ),
671
+ streamId
672
+ );
673
+ let outgoingBodyStreamId;
674
+ const responseOutparam = responseOutparamCreate((response) => {
675
+ if (response.tag === "ok") {
676
+ const outgoingResponse = response.val;
677
+ const statusCode = outgoingResponse.statusCode();
678
+ const headers = outgoingResponse.headers().entries();
679
+ const body = outgoingResponseBody(outgoingResponse);
680
+ outgoingBodyStreamId = outgoingBodyOutputStreamId(body);
681
+ ioCall(HTTP_SERVER_SET_OUTGOING_RESPONSE, responseId, {
682
+ statusCode,
683
+ headers,
684
+ streamId: outgoingBodyStreamId,
685
+ });
686
+ } else {
687
+ ioCall(HTTP_SERVER_CLEAR_OUTGOING_RESPONSE, responseId, null);
688
+ console.error(response.val);
689
+ process.exit(1);
690
+ }
691
+ });
692
+ incomingHandler.handle(request, responseOutparam);
693
+ if (outgoingBodyStreamId) {
694
+ ioCall(OUTPUT_STREAM_DISPOSE, outgoingBodyStreamId, null);
695
+ }
696
+ }
697
+ );
698
+ }
699
+ listen(port, host) {
700
+ // set a dummy interval, to keep the process alive since the server is off-thread
701
+ this.#liveEventLoopInterval = setInterval(() => {}, 10_000);
702
+ ioCall(HTTP_SERVER_START, this.#id, { port, host });
703
+ }
704
+ stop() {
705
+ clearInterval(this.#liveEventLoopInterval);
706
+ ioCall(HTTP_SERVER_STOP, this.#id, null);
707
+ httpServers.delete(this.#id);
708
+ }
709
+ }