@bytecodealliance/preview2-shim 0.0.21 → 0.14.1

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.
Files changed (44) hide show
  1. package/README.md +4 -14
  2. package/lib/browser/cli.js +2 -4
  3. package/lib/browser/clocks.js +15 -27
  4. package/lib/browser/filesystem.js +2 -30
  5. package/lib/browser/http.js +1 -3
  6. package/lib/browser/io.js +4 -2
  7. package/lib/common/assert.js +7 -0
  8. package/lib/io/calls.js +64 -0
  9. package/lib/io/worker-http.js +95 -0
  10. package/lib/io/worker-io.js +322 -0
  11. package/lib/io/worker-thread.js +569 -0
  12. package/lib/nodejs/cli.js +45 -59
  13. package/lib/nodejs/clocks.js +13 -27
  14. package/lib/nodejs/filesystem.js +539 -459
  15. package/lib/nodejs/http.js +440 -173
  16. package/lib/nodejs/index.js +4 -1
  17. package/lib/nodejs/io.js +1 -0
  18. package/lib/nodejs/sockets/socket-common.js +116 -0
  19. package/lib/nodejs/sockets/socketopts-bindings.js +94 -0
  20. package/lib/nodejs/sockets/tcp-socket-impl.js +794 -0
  21. package/lib/nodejs/sockets/udp-socket-impl.js +628 -0
  22. package/lib/nodejs/sockets/wasi-sockets.js +320 -0
  23. package/lib/nodejs/sockets.js +11 -200
  24. package/lib/synckit/index.js +4 -2
  25. package/package.json +1 -5
  26. package/types/interfaces/wasi-cli-terminal-input.d.ts +4 -0
  27. package/types/interfaces/wasi-cli-terminal-output.d.ts +4 -0
  28. package/types/interfaces/wasi-clocks-monotonic-clock.d.ts +19 -6
  29. package/types/interfaces/wasi-filesystem-types.d.ts +1 -178
  30. package/types/interfaces/wasi-http-outgoing-handler.d.ts +2 -2
  31. package/types/interfaces/wasi-http-types.d.ts +412 -82
  32. package/types/interfaces/wasi-io-error.d.ts +16 -0
  33. package/types/interfaces/wasi-io-poll.d.ts +19 -8
  34. package/types/interfaces/wasi-io-streams.d.ts +26 -46
  35. package/types/interfaces/wasi-sockets-ip-name-lookup.d.ts +9 -21
  36. package/types/interfaces/wasi-sockets-network.d.ts +4 -0
  37. package/types/interfaces/wasi-sockets-tcp.d.ts +75 -18
  38. package/types/interfaces/wasi-sockets-udp-create-socket.d.ts +1 -1
  39. package/types/interfaces/wasi-sockets-udp.d.ts +282 -193
  40. package/types/wasi-cli-command.d.ts +28 -28
  41. package/types/wasi-http-proxy.d.ts +12 -12
  42. package/lib/common/io.js +0 -183
  43. package/lib/common/make-request.js +0 -30
  44. package/types/interfaces/wasi-clocks-timezone.d.ts +0 -56
@@ -1,46 +1,30 @@
1
- import { streams } from '../common/io.js';
2
- import { fileURLToPath } from 'node:url';
3
- import { createSyncFn } from '../synckit/index.js';
1
+ import {
2
+ INPUT_STREAM_DISPOSE,
3
+ HTTP_CREATE_REQUEST,
4
+ HTTP_OUTPUT_STREAM_FINISH,
5
+ OUTPUT_STREAM_CREATE,
6
+ FUTURE_GET_VALUE_AND_DISPOSE,
7
+ FUTURE_DISPOSE,
8
+ } from "../io/calls.js";
9
+ import {
10
+ ioCall,
11
+ pollableCreate,
12
+ inputStreamCreate,
13
+ outputStreamCreate,
14
+ outputStreamId,
15
+ } from "../io/worker-io.js";
16
+ import { validateHeaderName, validateHeaderValue } from "node:http";
4
17
 
5
- const { InputStream, OutputStream } = streams;
18
+ import { HTTP } from "../io/calls.js";
19
+
20
+ const symbolDispose = Symbol.dispose || Symbol.for("dispose");
21
+ export const _forbiddenHeaders = new Set(["connection", "keep-alive"]);
6
22
 
7
23
  /**
8
24
  * @typedef {import("../../types/interfaces/wasi-http-types").Method} Method
9
- * @typedef {import("../../types/interfaces/wasi-http-types").RequestOptions} RequestOptions
10
25
  * @typedef {import("../../types/interfaces/wasi-http-types").Scheme} Scheme
11
26
  * @typedef {import("../../types/interfaces/wasi-http-types").Error} HttpError
12
- */
13
-
14
- const workerPath = fileURLToPath(new URL('../common/make-request.js', import.meta.url));
15
-
16
- function send(req) {
17
- const syncFn = createSyncFn(workerPath);
18
- let rawResponse = syncFn(req);
19
- let response = JSON.parse(rawResponse);
20
- if (response.status) {
21
- return {
22
- ...response,
23
- body: response.body ? Buffer.from(response.body, 'base64') : undefined,
24
- };
25
- }
26
- // HttpError
27
- throw { tag: 'unexpected-error', val: response.message };
28
- }
29
-
30
- function combineChunks (chunks) {
31
- if (chunks.length === 0)
32
- return new Uint8Array();
33
- if (chunks.length === 1)
34
- return chunks[0];
35
- const totalLen = chunks.reduce((total, chunk) => total + chunk.byteLength);
36
- const out = new Uint8Array(totalLen);
37
- let idx = 0;
38
- for (const chunk of chunks) {
39
- out.set(chunk, idx);
40
- idx += chunk.byteLength;
41
- }
42
- return out;
43
- }
27
+ */
44
28
 
45
29
  export class WasiHttp {
46
30
  requestCnt = 1;
@@ -52,49 +36,84 @@ export class WasiHttp {
52
36
  const http = this;
53
37
 
54
38
  class IncomingBody {
55
- #bodyFinished = false;
56
- #chunks = [];
57
- stream () {
58
- const incomingBody = this;
59
- return new InputStream({
60
- blockingRead (_len) {
61
- if (incomingBody.#bodyFinished)
62
- throw { tag: 'closed' };
63
- if (incomingBody.#chunks.length === 0)
64
- return new Uint8Array([]);
65
- // TODO: handle chunk splitting case where len is less than chunk length
66
- return incomingBody.#chunks.shift();
67
- },
68
- subscribe () {
69
- // TODO
70
- }
71
- });
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);
72
46
  }
73
- static finish (incomingBody) {
74
- incomingBody.#bodyFinished = true;
47
+ static finish(incomingBody) {
48
+ if (incomingBody.#finished)
49
+ throw new Error("incoming body already finished");
50
+ incomingBody.#finished = true;
75
51
  return futureTrailersCreate(new Fields([]), false);
76
52
  }
77
- static _addChunk (incomingBody, chunk) {
78
- incomingBody.#chunks.push(chunk);
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;
79
63
  }
80
64
  }
81
- const incomingBodyAddChunk = IncomingBody._addChunk;
82
- delete IncomingBody._addChunk;
65
+ const incomingBodyCreate = IncomingBody._create;
66
+ delete IncomingBody._create;
83
67
 
84
- // TODO
85
- class IncomingRequest {}
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;
86
105
 
87
106
  class FutureTrailers {
88
- id = this.futureCnt++;
107
+ _id = http.futureCnt++;
89
108
  #value;
90
109
  #isError;
91
- subscribe () {
110
+ subscribe() {
92
111
  // TODO
93
112
  }
94
- get () {
95
- return { tag: this.#isError ? 'err' : 'ok', val: this.#value };
113
+ get() {
114
+ return { tag: this.#isError ? "err" : "ok", val: this.#value };
96
115
  }
97
- static _create (value, isError) {
116
+ static _create(value, isError) {
98
117
  const res = new FutureTrailers();
99
118
  res.#value = value;
100
119
  res.#isError = isError;
@@ -105,72 +124,157 @@ export class WasiHttp {
105
124
  delete FutureTrailers._create;
106
125
 
107
126
  class OutgoingResponse {
108
- id = http.responseCnt++;
109
- /** @type {number} */ _statusCode;
110
- /** @type {Fields} */ _headers;
127
+ _id = http.responseCnt++;
128
+ /** @type {number} */ #statusCode = 200;
129
+ /** @type {Fields} */ #headers;
111
130
 
112
131
  /**
113
- * @param {number} statusCode
114
- * @param {Fields} headers
132
+ * @param {number} statusCode
133
+ * @param {Fields} headers
115
134
  */
116
- constructor (statusCode, headers) {
117
- this._statusCode = statusCode;
118
- this.headers = headers;
135
+ constructor(headers) {
136
+ fieldsLock(headers);
137
+ this.#headers = headers;
138
+ }
139
+
140
+ statusCode() {
141
+ return this.#statusCode;
142
+ }
143
+
144
+ setStatusCode(statusCode) {
145
+ this.#statusCode = statusCode;
146
+ }
147
+
148
+ headers() {
149
+ return this.#headers;
150
+ }
151
+
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);
119
162
  }
120
163
  }
121
164
 
122
165
  class ResponseOutparam {
123
- static set (_param, _response) {
124
- // TODO
166
+ #response;
167
+ static set(param, response) {
168
+ param.#response = response;
169
+ }
170
+ }
171
+
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;
125
193
  }
126
194
  }
127
195
 
128
196
  class OutgoingRequest {
129
- id = http.requestCnt++;
130
- /** @type {Method} */ #method = { tag: 'get' };
131
- /** @type {Scheme | undefined} */ #scheme = { tag: 'HTTP' };
197
+ _id = http.requestCnt++;
198
+ /** @type {Method} */ #method = { tag: "get" };
199
+ /** @type {Scheme | undefined} */ #scheme = undefined;
132
200
  /** @type {string | undefined} */ #pathWithQuery = undefined;
133
201
  /** @type {string | undefined} */ #authority = undefined;
134
- /** @type {Fields} */ #headers = undefined;
135
- /** @type {OutgoingBody} */ #body = new OutgoingBody();
136
- constructor(method, pathWithQuery, scheme, authority, headers) {
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;
137
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;
138
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;
139
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
+ }
140
257
  this.#authority = authority;
141
- this.#headers = headers;
142
258
  }
143
- write () {
144
- return this.#body;
259
+ headers() {
260
+ return this.#headers;
145
261
  }
146
-
147
- static _handle (request) {
148
- const scheme = request.#scheme.tag === "HTTP" ? "http://" : "https://";
149
- const url = scheme + request.#authority + request.#pathWithQuery;
150
- const headers = {
151
- "host": request.#authority,
152
- };
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]];
153
268
  const decoder = new TextDecoder();
154
269
  for (const [key, value] of request.#headers.entries()) {
155
- headers[key] = decoder.decode(value);
156
- }
157
-
158
- let res;
159
- try {
160
- res = send({
161
- method: request.#method.tag,
162
- uri: url,
163
- headers: headers,
164
- params: [],
165
- body: combineChunks(outgoingBodyGetChunks(request.#body)),
166
- });
167
- } catch (err) {
168
- return newFutureIncomingResponse(err.toString(), true);
270
+ headers.push([key, decoder.decode(value)]);
169
271
  }
170
-
171
- const encoder = new TextEncoder();
172
- const response = newIncomingResponse(res.status, new Fields(res.headers.map(([key, value]) => [key, encoder.encode(value)])), [res.body]);
173
- return newFutureIncomingResponse({ tag: 'ok', val: response }, false);
272
+ return futureIncomingResponseCreate(
273
+ request.#method.val || request.#method.tag,
274
+ url,
275
+ Object.entries(headers),
276
+ outgoingBodyOutputStreamId(request.#body)
277
+ );
174
278
  }
175
279
  }
176
280
 
@@ -178,121 +282,269 @@ export class WasiHttp {
178
282
  delete OutgoingRequest._handle;
179
283
 
180
284
  class OutgoingBody {
181
- #chunks = [];
182
- write () {
183
- const body = this;
184
- return new OutputStream({
185
- write (bytes) {
186
- body.#chunks.push(bytes);
187
- },
188
- blockingFlush () {}
189
- });
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]();
190
299
  }
191
300
  /**
192
- * @param {OutgoingBody} body
301
+ * @param {OutgoingBody} body
193
302
  * @param {Fields | undefined} trailers
194
303
  */
195
- static finish (_body, _trailers) {
196
- // TODO
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]();
197
314
  }
198
-
199
- static _getChunks (body) {
200
- return body.#chunks;
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;
201
323
  }
202
324
  }
203
- const outgoingBodyGetChunks = OutgoingBody._getChunks;
204
- delete OutgoingBody._getChunks;
325
+ const outgoingBodyOutputStreamId = OutgoingBody._outputStreamId;
326
+ delete OutgoingBody._outputStreamId;
327
+
328
+ const outgoingBodyCreate = OutgoingBody._create;
329
+ delete OutgoingBody._create;
205
330
 
206
331
  class IncomingResponse {
207
- id = http.responseCnt++;
208
- /** @type {InputStream} */ #body;
332
+ _id = http.responseCnt++;
209
333
  /** @type {Fields} */ #headers = undefined;
210
334
  #status = 0;
211
- #bodyConsumed = false;
212
- status () {
335
+ /** @type {number} */ #bodyStreamId;
336
+ status() {
213
337
  return this.#status;
214
338
  }
215
- headers () {
339
+ headers() {
216
340
  return this.#headers;
217
341
  }
218
- consume () {
219
- if (this.#bodyConsumed)
220
- throw { tag: 'unexpected-error', val: 'request body already consumed' };
221
- this.#bodyConsumed = true;
222
- return this.#body;
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
+ }
223
353
  }
224
- static _create (status, headers, chunks) {
354
+ static _create(status, headers, bodyStreamId) {
225
355
  const res = new IncomingResponse();
226
356
  res.#status = status;
227
357
  res.#headers = headers;
228
- res.#body = new IncomingBody();
229
- for (const chunk of chunks) {
230
- incomingBodyAddChunk(res.#body, chunk);
231
- }
358
+ res.#bodyStreamId = bodyStreamId;
232
359
  return res;
233
360
  }
234
361
  }
235
362
 
236
- const newIncomingResponse = IncomingResponse._create;
363
+ const incomingResponseCreate = IncomingResponse._create;
237
364
  delete IncomingResponse._create;
238
365
 
239
366
  class FutureIncomingResponse {
240
- id = this.futureCnt++;
241
- #value;
242
- #isError;
243
- subscribe () {
244
- // TODO
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
+ };
245
397
  }
246
- get () {
247
- return { tag: this.#isError ? 'err' : 'ok', val: this.#value };
398
+ [symbolDispose]() {
399
+ if (this.#pollId) ioCall(FUTURE_DISPOSE | HTTP, this.#pollId);
248
400
  }
249
- static _create (value, isError) {
401
+ static _create(method, url, headers, body) {
250
402
  const res = new FutureIncomingResponse();
251
- res.#value = value;
252
- res.#isError = isError;
403
+ res.#pollId = ioCall(HTTP_CREATE_REQUEST, null, {
404
+ method,
405
+ url,
406
+ headers,
407
+ body,
408
+ });
253
409
  return res;
254
410
  }
255
411
  }
256
412
 
257
- const newFutureIncomingResponse = FutureIncomingResponse._create;
413
+ const futureIncomingResponseCreate = FutureIncomingResponse._create;
258
414
  delete FutureIncomingResponse._create;
259
415
 
260
416
  class Fields {
261
- id = http.fieldsCnt++;
262
- /** @type {Record<string, Uint8Array[]>} */ #fields;
417
+ _id = http.fieldsCnt++;
418
+ #immutable = false;
419
+ /** @type {[string, Uint8Array[]][]} */ #entries = [];
420
+ /** @type {Map<string, [string, Uint8Array[]][]>} */ #table = new Map();
263
421
 
264
422
  /**
265
- * @param {[string, Uint8Array[]][]} fields
423
+ * @param {[string, Uint8Array[][]][]} entries
266
424
  */
267
- constructor(fields) {
268
- this.#fields = Object.fromEntries(fields);
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;
269
431
  }
270
- get (name) {
271
- return this.#fields[name];
432
+ get(name) {
433
+ const tableEntries = this.#table.get(name.toLowerCase());
434
+ if (!tableEntries) return [];
435
+ return tableEntries.map(([, v]) => v);
272
436
  }
273
- set (name, value) {
274
- this.#fields[name] = value;
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
+ }
275
465
  }
276
- delete (name) {
277
- delete this.#fields[name];
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
+ }
278
476
  }
279
- entries () {
280
- return Object.entries(this.#fields);
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);
281
505
  }
282
- clone () {
283
- return new Fields(this.entries());
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;
284
523
  }
285
524
  }
525
+ const fieldsLock = Fields._lock;
526
+ delete Fields._lock;
527
+ const fieldsFromEntriesSafe = Fields._fromEntriesChecked;
528
+ delete Fields._fromEntriesChecked;
286
529
 
287
530
  this.outgoingHandler = {
288
531
  /**
289
532
  * @param {OutgoingRequest} request
290
- * @param {RequestOptions | undefined} _options
533
+ * @param {RequestOptions | undefined} options
291
534
  * @returns {FutureIncomingResponse}
292
535
  */
293
- handle: outgoingRequestHandle
536
+ handle: outgoingRequestHandle,
294
537
  };
295
538
 
539
+ this._incomingRequestCreate = incomingRequestCreate;
540
+
541
+ function httpErrorCode(err) {
542
+ return {
543
+ tag: "internal-error",
544
+ val: err.message,
545
+ };
546
+ }
547
+
296
548
  this.types = {
297
549
  Fields,
298
550
  FutureIncomingResponse,
@@ -303,9 +555,24 @@ export class WasiHttp {
303
555
  OutgoingBody,
304
556
  OutgoingRequest,
305
557
  OutgoingResponse,
306
- ResponseOutparam
558
+ ResponseOutparam,
559
+ RequestOptions,
560
+ httpErrorCode,
307
561
  };
308
562
  }
309
563
  }
310
564
 
565
+ function schemeString(scheme) {
566
+ if (!scheme)
567
+ return 'https:';
568
+ switch (scheme.tag) {
569
+ case "HTTP":
570
+ return "http:";
571
+ case "HTTPS":
572
+ return "https:";
573
+ case "other":
574
+ return scheme.val.toLowerCase() + ":";
575
+ }
576
+ }
577
+
311
578
  export const { outgoingHandler, types } = new WasiHttp();