@statewalker/webrun-http-streams 0.1.1 → 0.2.2

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 (56) hide show
  1. package/README.md +361 -13
  2. package/dist/bytes.d.ts +43 -0
  3. package/dist/bytes.d.ts.map +1 -0
  4. package/dist/codec-default.d.ts +7 -0
  5. package/dist/codec-default.d.ts.map +1 -0
  6. package/dist/duplex-site-builder.d.ts +4 -1
  7. package/dist/duplex-site-builder.d.ts.map +1 -1
  8. package/dist/envelope.d.ts +10 -10
  9. package/dist/envelope.d.ts.map +1 -1
  10. package/dist/fetch.d.ts +3 -2
  11. package/dist/fetch.d.ts.map +1 -1
  12. package/dist/http-data.d.ts +11 -7
  13. package/dist/http-data.d.ts.map +1 -1
  14. package/dist/http-error.d.ts.map +1 -1
  15. package/dist/http-stubs.d.ts +12 -0
  16. package/dist/http-stubs.d.ts.map +1 -1
  17. package/dist/http1/chunked.d.ts +18 -0
  18. package/dist/http1/chunked.d.ts.map +1 -0
  19. package/dist/http1/decode.d.ts +5 -0
  20. package/dist/http1/decode.d.ts.map +1 -0
  21. package/dist/http1/encode.d.ts +20 -0
  22. package/dist/http1/encode.d.ts.map +1 -0
  23. package/dist/http1/errors.d.ts +9 -0
  24. package/dist/http1/errors.d.ts.map +1 -0
  25. package/dist/http1/headers.d.ts +78 -0
  26. package/dist/http1/headers.d.ts.map +1 -0
  27. package/dist/http1/index.d.ts +18 -0
  28. package/dist/http1/index.d.ts.map +1 -0
  29. package/dist/index.d.ts +6 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1040 -138
  32. package/dist/message.d.ts +50 -0
  33. package/dist/message.d.ts.map +1 -0
  34. package/dist/request-streams.d.ts +52 -0
  35. package/dist/request-streams.d.ts.map +1 -0
  36. package/dist/sniff.d.ts +18 -0
  37. package/dist/sniff.d.ts.map +1 -0
  38. package/package.json +13 -7
  39. package/src/bytes.ts +157 -0
  40. package/src/codec-default.ts +13 -0
  41. package/src/duplex-site-builder.ts +10 -2
  42. package/src/envelope.ts +43 -34
  43. package/src/fetch.ts +130 -12
  44. package/src/http-data.ts +160 -17
  45. package/src/http-stubs.ts +72 -18
  46. package/src/http1/chunked.ts +89 -0
  47. package/src/http1/decode.ts +208 -0
  48. package/src/http1/encode.ts +181 -0
  49. package/src/http1/errors.ts +8 -0
  50. package/src/http1/headers.ts +263 -0
  51. package/src/http1/index.ts +40 -0
  52. package/src/index.ts +18 -0
  53. package/src/message.ts +62 -0
  54. package/src/request-streams.ts +75 -0
  55. package/src/sniff.ts +68 -0
  56. package/LICENSE +0 -21
package/README.md CHANGED
@@ -1,50 +1,398 @@
1
1
  # @statewalker/webrun-http-streams
2
2
 
3
- HTTP request / response over a `Duplex` from any `webrun-streams-*` adapter. Replaces `webrun-http` + `webrun-http-port`.
3
+ HTTP request / response over a `Duplex` from any `webrun-streams-*` adapter.
4
+ Replaces the retired `webrun-http` + `webrun-http-port` packages.
4
5
 
5
- ## Three layers
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install @statewalker/webrun-http-streams
10
+ ```
11
+
12
+ One runtime dependency, [`@statewalker/webrun-streams`](../webrun-streams); no
13
+ peer dependencies. ESM only (`"type": "module"`). Needs standard `Request`,
14
+ `Response`, `ReadableStream`, `TextEncoder` and `TextDecoder` — present in
15
+ browsers, Node 18+, Deno, Bun and Cloudflare Workers.
16
+
17
+ Pair it with a transport adapter to get a `Duplex`:
18
+ [`-ws`](../webrun-streams-ws), [`webrun-rpc`](../webrun-rpc),
19
+ [`-webrtc`](../webrun-streams-webrtc), [`-libp2p`](../webrun-streams-libp2p),
20
+ [`-livekit`](../webrun-streams-livekit), [`-peerjs`](../webrun-streams-peerjs).
21
+
22
+ ## Getting started
23
+
24
+ Pick a transport, then move standard `Request` / `Response` objects across it.
25
+ Here the transport is an in-process `MessageChannel`, so the whole exchange runs
26
+ in one process with no network:
27
+
28
+ ```ts
29
+ import { connect, serve } from "@statewalker/webrun-rpc";
30
+ import { fetchOverDuplex, serveFetchOverDuplex } from "@statewalker/webrun-http-streams";
31
+
32
+ // --- server side ---
33
+ const channel = new MessageChannel();
34
+ const stop = await serve(
35
+ { port: channel.port2 },
36
+ serveFetchOverDuplex(async (request) => {
37
+ const url = new URL(request.url);
38
+ return Response.json({ path: url.pathname, method: request.method });
39
+ }),
40
+ );
41
+
42
+ // --- client side ---
43
+ const { call, close } = await connect({ port: channel.port1 });
44
+ const response = await fetchOverDuplex(call, new Request("http://local/api/todo/7"));
45
+
46
+ console.log(response.status); // 200
47
+ console.log(await response.json()); // { path: "/api/todo/7", method: "GET" }
48
+
49
+ await close();
50
+ await stop();
51
+ ```
52
+
53
+ Swap `@statewalker/webrun-rpc` for
54
+ [`-ws`](../webrun-streams-ws), [`-webrtc`](../webrun-streams-webrtc),
55
+ [`-libp2p`](../webrun-streams-libp2p), [`-livekit`](../webrun-streams-livekit)
56
+ or [`-peerjs`](../webrun-streams-peerjs) and nothing else changes — that is the
57
+ whole point of the `Duplex` seam.
58
+
59
+ Streaming bodies work as you would expect: a `Response` whose body is a
60
+ `ReadableStream` streams across the transport chunk by chunk, which is what
61
+ makes server-sent events work over a `MessagePort` or a WebRTC link.
62
+
63
+ ## Layers
64
+
65
+ Three of them sit on the `Duplex` seam, plus one older transport-agnostic pair
66
+ that predates it and is kept for `webrun-http-browser`.
6
67
 
7
68
  ### Data layer — `httpFetch` / `httpServe`
8
69
 
70
+ Envelopes and body iterators, no `Request`/`Response` involved.
71
+
9
72
  ```ts
10
73
  import { httpFetch, httpServe } from "@statewalker/webrun-http-streams";
11
- import { connect } from "@statewalker/webrun-streams-ws";
12
74
 
13
- const { call } = await connect({ url });
75
+ // `call` is a Duplex. In-process it can be the handler itself; over a
76
+ // transport it comes from an adapter's `connect`:
77
+ // const { call } = await connect({ url }); // webrun-streams-ws
78
+ const call = httpServe(async (env, body) => {
79
+ for await (const _chunk of body) {
80
+ /* drain the request body */
81
+ }
82
+ return {
83
+ envelope: { status: 200, statusText: "OK", headers: [["content-type", "text/plain"]] },
84
+ body: [new TextEncoder().encode(`hello ${new URL(env.url).pathname}`)],
85
+ };
86
+ });
87
+
14
88
  const { envelope, body } = await httpFetch(call, {
15
- url: "/api/time",
89
+ url: "http://peer.test/api/time",
16
90
  method: "GET",
17
91
  headers: [],
18
92
  });
93
+ // envelope.status === 200; `body` is an AsyncIterable<Uint8Array>
19
94
  ```
20
95
 
21
- `httpServe(handler)` returns a `Duplex` you can hand to any adapter's `serve(...)`.
96
+ `httpServe(handler)` returns a `Duplex` you can hand to any adapter's
97
+ `serve(...)`. Because a `Duplex` is just `(input) => AsyncGenerator<Uint8Array>`,
98
+ the handler side *is* a usable `call` with no transport at all — that is what
99
+ the snippet above does, and it is the cheapest way to test a handler.
100
+
101
+ A relative `url` in the envelope works, but note it does not survive the round
102
+ trip verbatim: HTTP/1.1 origin-form carries no scheme or authority, so the
103
+ decoder rebuilds an absolute url from the codec's `scheme`/`host` options.
104
+ `url: "/api/time"` arrives at the handler as `http://localhost/api/time` unless
105
+ you configure the codec (see below).
22
106
 
23
107
  ### Fetch layer — `fetchOverDuplex` / `serveFetchOverDuplex`
24
108
 
109
+ The same thing in terms of standard `Request` / `Response`.
110
+
25
111
  ```ts
26
- const response = await fetchOverDuplex(call, new Request("/api/time"));
112
+ import { fetchOverDuplex, serveFetchOverDuplex } from "@statewalker/webrun-http-streams";
113
+
114
+ const call = serveFetchOverDuplex(
115
+ async (request) => new Response(`hello ${new URL(request.url).pathname}`),
116
+ );
117
+ const response = await fetchOverDuplex(call, new Request("http://peer.test/api/time"));
118
+ await response.text(); // "hello /api/time"
27
119
  ```
28
120
 
29
- `serveFetchOverDuplex(handler)` adapts a `(Request) => Promise<Response>` handler.
121
+ Hop-by-hop headers (`connection`, `transfer-encoding`, `host`, …) are stripped
122
+ in both directions: the codec surfaces them verbatim, but they are meaningless
123
+ to a `Request`/`Response` and re-emitting them from a relay would corrupt its
124
+ framing.
125
+
126
+ `fetchOverDuplex` plumbs `request.signal` into body iteration, so aborting the
127
+ signal terminates the call.
30
128
 
31
129
  ### Site host — `DuplexSiteBuilder`
32
130
 
33
131
  ```ts
34
132
  import { DuplexSiteBuilder } from "@statewalker/webrun-http-streams";
35
- import { serve } from "@statewalker/webrun-streams-port";
133
+ import { serve } from "@statewalker/webrun-rpc";
36
134
 
37
135
  const stop = await new DuplexSiteBuilder()
38
- .setHandler(siteHandler)
136
+ .setHandler(siteHandler) // (Request) => Promise<Response>
39
137
  .start(serve, { port });
138
+ // later: await stop();
40
139
  ```
41
140
 
42
- `DuplexSiteBuilder` is the cross-platform sibling of `HostedSiteBuilder` (browser+SW) — same `SiteHandler` seam, different transport.
141
+ `.setCodec(codec)` pins the wire format. `DuplexSiteBuilder` is the
142
+ cross-platform sibling of `HostedSiteBuilder` (browser + ServiceWorker) — same
143
+ `SiteHandler` seam, different transport. It holds no site configuration of its
144
+ own; endpoints, files and auth belong to the `SiteHandler` producer (typically
145
+ `SiteBuilder`).
146
+
147
+ > Not runnable as written above: it needs a live `MessagePort` and
148
+ > `@statewalker/webrun-rpc`, which this package does not depend on.
149
+ > `start` accepts any `Serve<P>`, so an in-process one is enough to exercise
150
+ > it. Calling `start()` before `setHandler()` throws.
151
+
152
+ ### Transport-agnostic stubs — `newHttpClientStub` / `newHttpServerStub`
153
+
154
+ Predates the `Duplex` seam and does **not** use it. Instead of bytes on a wire,
155
+ these move a `SerializedHttpEnvelope` — a plain options object plus a body
156
+ iterable — over whatever `send` function you give them. `webrun-http-browser`
157
+ is built on this pair, because a MessagePort can structured-clone the envelope
158
+ directly and never needs a byte encoding.
159
+
160
+ ```ts
161
+ import { newHttpClientStub, newHttpServerStub } from "@statewalker/webrun-http-streams";
162
+
163
+ const server = newHttpServerStub(async (request) => new Response(`echo ${await request.text()}`));
164
+ const client = newHttpClientStub(server); // `send` is anything envelope-in, envelope-out
165
+
166
+ const res = await client(new Request("http://peer.test/x", { method: "POST", body: "hi" }));
167
+ await res.text(); // "echo hi"
168
+ ```
169
+
170
+ A `send` that resolves `undefined` — no service registered for this call —
171
+ becomes a `404` on the client side. `SerializedHttpEnvelope.content` must be
172
+ *productive*: it has to yield or finish on its own, because a body neither stub
173
+ is allowed to read is released by draining it, not by a bare `.return()`.
174
+
175
+ Use the `Duplex` layers for anything new. These stay because the browser
176
+ package's MessagePort transport is built around them.
43
177
 
44
178
  ## Wire format
45
179
 
46
- `<JSON.stringify(envelope)>\n<body bytes…>` — newline-delimited JSON header followed by raw body bytes. Same shape as the legacy `webrun-http-port` so a future bridging adapter could interop.
180
+ Conforming **HTTP/1.1** by default — a `Duplex` carries bytes a real HTTP
181
+ implementation can parse, and accepts bytes a real HTTP implementation
182
+ produces. Verified against `node:http` in both directions.
183
+
184
+ ```
185
+ POST /api?a=1 HTTP/1.1
186
+ Host: peer.test
187
+ Connection: close
188
+ Transfer-Encoding: chunked
189
+
190
+ 5
191
+ hello
192
+ 0
193
+
194
+ ```
195
+
196
+ One message per `Duplex` call: `Connection: close` is always emitted, and bytes
197
+ after a complete message are an error. Bodies use `Content-Length` when the
198
+ caller declares one and chunked transfer coding otherwise; a message declaring
199
+ both is refused.
200
+
201
+ The codec is deliberately strict: every ambiguity is a refusal rather than a
202
+ guess. One consequence worth calling out explicitly — RFC 9112 §2.2 permits a
203
+ recipient to *tolerate* a single leading CRLF sent before the request-line
204
+ ("SHOULD ignore"). This codec declines that leniency and refuses it as a
205
+ malformed request line; see ADR-0006.
206
+
207
+ ### Bodyless messages
208
+
209
+ Responses with a null-body status — **204, 205, 304** — and responses to `HEAD`
210
+ or `OPTIONS` put no bytes on the wire, in both directions, even if the
211
+ handler's `Response` carries a body stream. The handler's stream is cancelled
212
+ rather than sent. On the reading side you get `new Response(null, init)`, whose
213
+ `.body` is `null`.
214
+
215
+ The internal null-body set also lists 101 and 103, but neither is reachable
216
+ through the fetch layer: `ResponseInit.status` must be 200–599, so
217
+ `fetchOverDuplex` throws a `TypeError` from the `Response` constructor if a
218
+ peer answers with one. Use the data layer (`httpFetch`) if you need to see an
219
+ informational status.
220
+
221
+ ### Choosing a codec
222
+
223
+ ```ts
224
+ import {
225
+ defaultCodec,
226
+ httpCodec,
227
+ jsonEnvelopeCodec,
228
+ newHttpCodec,
229
+ newSniffingCodec,
230
+ } from "@statewalker/webrun-http-streams";
231
+
232
+ // default: writes HTTP/1.1, accepts HTTP/1.1 or the legacy JSON envelope
233
+ await httpFetch(call, env);
234
+
235
+ // pinned
236
+ await httpFetch(call, env, body, { codec: httpCodec });
237
+
238
+ // the scheme and authority are not on the HTTP/1.1 wire; supply them here
239
+ const codec = newHttpCodec({ scheme: "https", host: "peer.test" });
240
+ ```
241
+
242
+ | `newHttpCodec` option | Default | Meaning |
243
+ | --- | --- | --- |
244
+ | `scheme` | `"http"` | Scheme used to rebuild an absolute url on decode. Configuration, not wire data — a peer configured `http` rebuilds an `https` url as `http`. |
245
+ | `host` | `"localhost"` | Authority used when a url carries none. Also fills the mandatory `Host` header. |
246
+ | `maxHeaderBytes` | `65536` | Bound on the whole head section, start line included. |
247
+
248
+ `httpCodec` is `newHttpCodec()` with those defaults. `defaultCodec` — used
249
+ whenever no `codec` option is supplied — is
250
+ `newSniffingCodec({ write: httpCodec, accept: [httpCodec, jsonEnvelopeCodec] })`.
251
+
252
+ The previous format — `<JSON.stringify(envelope)>` + newline + body bytes —
253
+ remains available as `jsonEnvelopeCodec` (and as the raw `encodeMessage` /
254
+ `decodeMessage` pair), and readers accept it automatically, so the two ends of
255
+ a peer pair can be upgraded in either order. A server answers in whichever
256
+ format read the request. Sniffing needs no handshake because the formats are
257
+ self-identifying: a JSON envelope always begins `{`, which is not a token
258
+ character and so can never begin an HTTP start-line. See ADR-0006.
259
+
260
+ ## Browser support
261
+
262
+ Everything here is built on `Request`, `Response`, `ReadableStream`,
263
+ `TextEncoder` and `TextDecoder`. Browsers do **not** behave uniformly, and one
264
+ difference is large enough to change what this package can do.
265
+
266
+ **Firefox does not implement `Request.prototype.body`** (checked against
267
+ Firefox 146). `Object.getOwnPropertyDescriptor(Request.prototype, "body")` is
268
+ `null` — the accessor is genuinely absent — and because a `ReadableStream` is
269
+ then not a recognised `BodyInit`, `new Request(url, { body: stream })` falls
270
+ through to the string branch and stores the literal text
271
+ `[object ReadableStream]`.
272
+
273
+ Both directions have a fallback, and both cost the same thing:
274
+
275
+ - **Sending** (`fetchOverDuplex`, `newHttpClientStub`) — with no
276
+ `request.body` to stream from, the whole payload is buffered with
277
+ `request.arrayBuffer()` before it goes on the wire.
278
+ - **Receiving** (`serveFetchOverDuplex`, `newHttpServerStub`) — the request
279
+ body is drained into one contiguous buffer before the `Request` handed to
280
+ your handler is constructed. Without this the handler would silently read the
281
+ string `[object ReadableStream]` and answer `200` with corrupt data.
282
+
283
+ So on Firefox: **request streaming does not happen**. A 1 GiB upload from a
284
+ Firefox page is a 1 GiB allocation, where Chromium and Node stream it chunk by
285
+ chunk. Response streaming is unaffected — `Response.body` exists everywhere.
286
+
287
+ The check is a capability probe, not a user-agent test, so it flips on its own
288
+ the day Firefox ships request streams. Safari is untested and is the case to
289
+ look at first if a report arrives: it has had `Request.body` since 11.1 but
290
+ only accepts a stream as `init.body` from Technology Preview 250, so a shipping
291
+ Safari has the reader half without the upload half and takes the streaming
292
+ branch here.
293
+
294
+ One further divergence, on the sending side only: buffering cannot distinguish
295
+ an absent body from an empty one. Chromium's
296
+ `new Request(url, { method: "POST", body: "" })` yields a non-null empty
297
+ stream, so an empty body is framed on the wire; on the Firefox path a
298
+ zero-length `arrayBuffer()` is indistinguishable from no body and none is sent.
299
+ Both decode to the same empty body at the far end, so only the framing differs.
300
+ This does not arise for the stubs, whose envelope always carries a `content`
301
+ iterable.
302
+
303
+ ## Errors
304
+
305
+ The codec answers on the wire wherever it can, because a real HTTP peer cannot
306
+ receive a JavaScript exception — only a response, or a connection that ends
307
+ with no status.
308
+
309
+ | Situation | On the wire | On a webrun caller |
310
+ | --- | --- | --- |
311
+ | Handler throws | `500 Internal Server Error`, message in the body | `httpFetch` rejects with the peer's error, stack and custom fields preserved |
312
+ | Request cannot be parsed | `400 Bad Request`, reason in the body | `httpFetch` rejects with the parse error |
313
+ | Transport fails | nothing — the failure is not ours to answer | the error propagates unchanged |
314
+
315
+ Both error responses carry the serialized error in an `x-webrun-error` header
316
+ (exported as `PEER_ERROR_HEADER`), which is what lets a webrun peer re-throw a
317
+ real `Error` rather than a status code. A third party just reads a conforming
318
+ response and ignores the header. The header value is bounded at 4096
319
+ characters, so a very large error message is truncated rather than blowing past
320
+ `maxHeaderBytes` at the reader.
321
+
322
+ Refusals are always `HttpParseError`, whichever codec read the message, so a
323
+ consumer can tell "the peer sent something malformed" from "the transport
324
+ broke". **Check `err.name`, not `instanceof`** — the two cases differ:
325
+
326
+ ```ts
327
+ import { httpFetch } from "@statewalker/webrun-http-streams";
328
+
329
+ try {
330
+ await httpFetch(call, env);
331
+ } catch (err) {
332
+ if ((err as Error).name === "HttpParseError") {
333
+ // malformed bytes — one side or an intermediary is at fault
334
+ } else {
335
+ throw err; // transport failure, or the peer handler's own error
336
+ }
337
+ }
338
+ ```
339
+
340
+ `instanceof HttpParseError` holds only when *this* side's decoder refused what
341
+ the peer sent. When the peer refuses **your** request, its 400 travels back
342
+ through the `x-webrun-error` header and is rehydrated by `deserializeError`,
343
+ which builds a plain `Error` carrying the original `name`, `message`, `stack`
344
+ and custom fields — not the original class. So `err.name` is
345
+ `"HttpParseError"` in both directions, and `instanceof` is true in only one.
346
+ The same applies to every error class crossing this boundary, including your
347
+ own handler's `Error` subclasses.
348
+
349
+ A refusal is answered in whatever format the peer was speaking, so a caller
350
+ pinned to the legacy envelope receives its 400 as an envelope rather than as
351
+ HTTP/1.1.
352
+
353
+ `HttpError` is a separate, unrelated helper for handlers that want to raise a
354
+ status deliberately (`HttpError.errorResourceNotFound()` and friends). It is
355
+ not wired into the wire format: throwing one produces a 500 like any other
356
+ exception.
357
+
358
+ ## Exports
359
+
360
+ | Export | What it is |
361
+ | --- | --- |
362
+ | `httpFetch`, `httpServe` | Data layer. Types: `HttpDataHandler`, `HttpDataHandlerResult`, `HttpDataOptions`, `HttpFetchResult`. |
363
+ | `fetchOverDuplex`, `serveFetchOverDuplex` | Fetch layer. |
364
+ | `DuplexSiteBuilder`, `SiteHandler` | Site host over a `Connect`/`Serve` pair. |
365
+ | `newHttpClientStub`, `newHttpServerStub` | Transport-agnostic stubs. Types: `HttpHandler`, `SerializedHttpEnvelope`, `SerializedHttpRequest`, `SerializedHttpResponse`. |
366
+ | `defaultCodec`, `httpCodec`, `newHttpCodec`, `HttpCodecOptions` | HTTP/1.1 codec and the default sniffing codec. |
367
+ | `jsonEnvelopeCodec`, `encodeMessage`, `decodeMessage` | Legacy JSON-envelope format. |
368
+ | `newSniffingCodec`, `SniffingCodecOptions` | Build your own write/accept combination. |
369
+ | `HttpParseError` | Every codec refusal. |
370
+ | `HttpError`, `HttpErrorOptions` | Status-raising helper for handlers; not wired into the wire format. |
371
+ | `PEER_ERROR_HEADER` | `"x-webrun-error"`. |
372
+ | `MessageCodec`, `ByteSource`, `RequestEnvelope`, `ResponseEnvelope`, `DecodedRequest`, `DecodedResponse`, `ResponseCodecOptions` | The codec seam's types. |
373
+
374
+ ## Dependencies
375
+
376
+ | Dependency | Kind | Why |
377
+ | --- | --- | --- |
378
+ | [`@statewalker/webrun-streams`](../webrun-streams) | runtime | The `Duplex` seam, chunk collectors and error serialisation. |
379
+
380
+ No peer dependencies and no runtime dependencies outside the workspace. Relies
381
+ on the platform for `Request`, `Response`, `ReadableStream`, `TextEncoder` and
382
+ `TextDecoder`.
383
+
384
+ Downstream consumers in this workspace:
385
+ [`webrun-http-browser`](../webrun-http-browser) and
386
+ [`webrun-rpc-http`](../webrun-rpc-http).
387
+
388
+ ## Scripts
389
+
390
+ ```sh
391
+ pnpm test # vitest run
392
+ pnpm run build # rolldown + tsc --emitDeclarationOnly
393
+ pnpm lint # biome check src tests
394
+ ```
47
395
 
48
396
  ## License
49
397
 
50
- MIT
398
+ MIT © statewalker — see [LICENSE](../../LICENSE).
@@ -0,0 +1,43 @@
1
+ import type { ByteSource } from "./message.js";
2
+ export declare class ByteStreamError extends Error {
3
+ readonly name = "ByteStreamError";
4
+ }
5
+ export declare function toAsyncIterator(input: ByteSource): AsyncIterator<Uint8Array>;
6
+ /**
7
+ * Discard an iterable we are contractually forbidden from consuming — a body
8
+ * skipped for HEAD/204, or one abandoned because the peer reported an error.
9
+ *
10
+ * The `.next()` is not optional: `.return()` on a generator still in suspended
11
+ * start is a no-op, so the body never runs and its `try/finally` never unwinds.
12
+ * Without it a wrapped ReadableStream or socket is never cancelled.
13
+ */
14
+ export declare function discard(source: ByteSource | undefined): Promise<void>;
15
+ export declare function concatChunks(parts: Uint8Array[], totalLen: number): Uint8Array;
16
+ /**
17
+ * Pull-based reader over a byte source. Holds at most one pending buffer, and
18
+ * hands out `subarray` views rather than copies — a body never passes through
19
+ * an allocation here.
20
+ */
21
+ export declare class ByteReader {
22
+ #private;
23
+ constructor(input: ByteSource);
24
+ /** Bytes already pulled from the source but not yet consumed. */
25
+ bufferedLength(): number;
26
+ peekByte(): Promise<number | undefined>;
27
+ /** Up to `max` bytes. `undefined` means end of stream. */
28
+ readSome(max: number): Promise<Uint8Array | undefined>;
29
+ /**
30
+ * One CRLF-terminated line, without the CRLF. A bare LF is rejected: real
31
+ * peers always send CRLF, and tolerating a bare LF is precisely the lenience
32
+ * that lets request smuggling through a proxy pair.
33
+ *
34
+ * The `maxBytes` bound is best-effort: it only rejects a line if the check
35
+ * happens to run before the line is fully buffered. A line already sitting in
36
+ * the buffer bypasses the check. Callers needing a hard per-line limit or
37
+ * aggregate bounds must keep their own running total.
38
+ */
39
+ readLine(maxBytes: number): Promise<Uint8Array>;
40
+ /** Everything not yet consumed, lazily. */
41
+ rest(): AsyncGenerator<Uint8Array>;
42
+ }
43
+ //# sourceMappingURL=bytes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAM/C,qBAAa,eAAgB,SAAQ,KAAK;IACxC,SAAkB,IAAI,qBAAqB;CAC5C;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAAC,UAAU,CAAC,CAS5E;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAc9E;AAED;;;;GAIG;AACH,qBAAa,UAAU;;IAKrB,YAAY,KAAK,EAAE,UAAU,EAE5B;IAED,iEAAiE;IACjE,cAAc,IAAI,MAAM,CAEvB;IAqBK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAK5C;IAED,0DAA0D;IACpD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAQ3D;IAED;;;;;;;;;OASG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAoBpD;IAED,2CAA2C;IACpC,IAAI,IAAI,cAAc,CAAC,UAAU,CAAC,CAUxC;CACF"}
@@ -0,0 +1,7 @@
1
+ import type { MessageCodec } from "./message.js";
2
+ /**
3
+ * Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
4
+ * no `codec` option is supplied.
5
+ */
6
+ export declare const defaultCodec: MessageCodec;
7
+ //# sourceMappingURL=codec-default.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec-default.d.ts","sourceRoot":"","sources":["../src/codec-default.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGjD;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,YAGzB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import type { Serve } from "@statewalker/webrun-streams";
2
+ import type { MessageCodec } from "./message.js";
2
3
  /**
3
4
  * Structural alias of `SiteHandler` from `@statewalker/webrun-site-builder`.
4
5
  * Kept local so this package doesn't depend on `webrun-site-builder`.
@@ -11,7 +12,7 @@ export type SiteHandler = (request: Request) => Promise<Response>;
11
12
  * ```ts
12
13
  * import { SiteBuilder } from "@statewalker/webrun-site-builder";
13
14
  * import { DuplexSiteBuilder } from "@statewalker/webrun-http-streams";
14
- * import { serve } from "@statewalker/webrun-streams-port";
15
+ * import { serve } from "@statewalker/webrun-rpc";
15
16
  *
16
17
  * const handler = new SiteBuilder()
17
18
  * .setEndpoint("/api/time", () => new Response(new Date().toISOString()))
@@ -27,6 +28,8 @@ export type SiteHandler = (request: Request) => Promise<Response>;
27
28
  export declare class DuplexSiteBuilder {
28
29
  #private;
29
30
  setHandler(handler: SiteHandler): this;
31
+ /** Pin the wire format. Defaults to HTTP/1.1 with legacy acceptance. */
32
+ setCodec(codec: MessageCodec): this;
30
33
  start<P>(serve: Serve<P>, params: P): Promise<() => Promise<void>>;
31
34
  }
32
35
  //# sourceMappingURL=duplex-site-builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplex-site-builder.d.ts","sourceRoot":"","sources":["../src/duplex-site-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAGzD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;;IAG5B,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAKhC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAUzE"}
1
+ {"version":3,"file":"duplex-site-builder.d.ts","sourceRoot":"","sources":["../src/duplex-site-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;;IAI5B,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAGrC;IAED,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAGlC;IAEK,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CASvE;CACF"}
@@ -1,13 +1,5 @@
1
- export type RequestEnvelope = {
2
- url: string;
3
- method: string;
4
- headers: [string, string][];
5
- };
6
- export type ResponseEnvelope = {
7
- status: number;
8
- statusText: string;
9
- headers: [string, string][];
10
- };
1
+ import type { MessageCodec } from "./message.js";
2
+ export type { RequestEnvelope, ResponseEnvelope } from "./message.js";
11
3
  /**
12
4
  * Encode an HTTP envelope plus optional body as one continuous byte stream:
13
5
  *
@@ -27,4 +19,12 @@ export declare function decodeMessage<E>(input: AsyncIterable<Uint8Array> | Iter
27
19
  envelope: E;
28
20
  body: AsyncIterable<Uint8Array>;
29
21
  }>;
22
+ /**
23
+ * The original wire format — `<JSON.stringify(envelope)>\n<body bytes…>` —
24
+ * expressed as a `MessageCodec`. Direction-agnostic: requests and responses
25
+ * serialise identically.
26
+ *
27
+ * Retained so a peer pair can be upgraded in either order; see ADR-0006.
28
+ */
29
+ export declare const jsonEnvelopeCodec: MessageCodec;
30
30
  //# sourceMappingURL=envelope.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;CAC7B,CAAC;AAMF;;;;;;;;;GASG;AACH,wBAAuB,aAAa,CAAC,CAAC,EACpC,QAAQ,EAAE,CAAC,EACX,IAAI,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,GACtD,cAAc,CAAC,UAAU,CAAC,CAM5B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,GACtD,OAAO,CAAC;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;CAAE,CAAC,CAgD3D"}
1
+ {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAIV,YAAY,EAGb,MAAM,cAAc,CAAC;AAEtB,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAStE;;;;;;;;;GASG;AACH,wBAAuB,aAAa,CAAC,CAAC,EACpC,QAAQ,EAAE,CAAC,EACX,IAAI,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,GACtD,cAAc,CAAC,UAAU,CAAC,CAM5B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,GACtD,OAAO,CAAC;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;CAAE,CAAC,CAyD3D;AAID;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAS/B,CAAC"}
package/dist/fetch.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import type { Duplex } from "@statewalker/webrun-streams";
2
+ import { type HttpDataOptions } from "./http-data.js";
2
3
  /**
3
4
  * Run a `Request` through a `Duplex` call and reconstruct the `Response` on
4
5
  * the other side. The request's `signal` is plumbed into the body iteration —
5
6
  * abort terminates the underlying call.
6
7
  */
7
- export declare function fetchOverDuplex(call: Duplex, request: Request): Promise<Response>;
8
+ export declare function fetchOverDuplex(call: Duplex, request: Request, options?: HttpDataOptions): Promise<Response>;
8
9
  /**
9
10
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
10
11
  * registered with any `webrun-streams-*` adapter's `serve`.
11
12
  */
12
- export declare function serveFetchOverDuplex(handler: (request: Request) => Promise<Response>): Duplex;
13
+ export declare function serveFetchOverDuplex(handler: (request: Request) => Promise<Response>, options?: HttpDataOptions): Duplex;
13
14
  //# sourceMappingURL=fetch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAgE1D;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAcvF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,CAsB7F"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAE1D,OAAO,EAAE,KAAK,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAsF5E;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA0EnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAChD,OAAO,GAAE,eAAoB,GAC5B,MAAM,CAiDR"}
@@ -1,12 +1,18 @@
1
1
  import type { Duplex } from "@statewalker/webrun-streams";
2
- import { type RequestEnvelope, type ResponseEnvelope } from "./envelope.js";
2
+ import type { ByteSource, MessageCodec, RequestEnvelope, ResponseEnvelope } from "./message.js";
3
+ /** Carries the serialized JS error between two webrun peers. */
4
+ export declare const PEER_ERROR_HEADER = "x-webrun-error";
5
+ export type HttpDataOptions = {
6
+ /** Wire format. Defaults to `defaultCodec`: writes HTTP/1.1, accepts either. */
7
+ codec?: MessageCodec;
8
+ };
3
9
  export interface HttpFetchResult {
4
10
  envelope: ResponseEnvelope;
5
11
  body: AsyncIterable<Uint8Array>;
6
12
  }
7
13
  export interface HttpDataHandlerResult {
8
14
  envelope: ResponseEnvelope;
9
- body?: AsyncIterable<Uint8Array> | Iterable<Uint8Array>;
15
+ body?: ByteSource;
10
16
  }
11
17
  /**
12
18
  * Low-level HTTP-over-Duplex handler. Takes the request envelope plus body
@@ -25,12 +31,10 @@ export type HttpDataHandler = (env: RequestEnvelope, body: AsyncIterable<Uint8Ar
25
31
  * The call is one logical Duplex invocation; multiplexing of concurrent
26
32
  * calls is the adapter's concern (native or `emulateMux`).
27
33
  */
28
- export declare function httpFetch(call: Duplex, env: RequestEnvelope, body?: AsyncIterable<Uint8Array> | Iterable<Uint8Array>): Promise<HttpFetchResult>;
34
+ export declare function httpFetch(call: Duplex, env: RequestEnvelope, body?: ByteSource, options?: HttpDataOptions): Promise<HttpFetchResult>;
29
35
  /**
30
36
  * Wrap an HTTP handler as a `Duplex` so it can be registered with any
31
- * `webrun-streams-*` adapter's `serve`. The duplex `split`s the input to
32
- * recover envelope + body, dispatches to the handler, and emits the response
33
- * via `encodeMessage`.
37
+ * `webrun-streams-*` adapter's `serve`.
34
38
  */
35
- export declare function httpServe(handler: HttpDataHandler): Duplex;
39
+ export declare function httpServe(handler: HttpDataHandler, options?: HttpDataOptions): Duplex;
36
40
  //# sourceMappingURL=http-data.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-data.d.ts","sourceRoot":"","sources":["../src/http-data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;CACzD;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,KAC5B,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAEpC;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,eAAe,EACpB,IAAI,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,GACtD,OAAO,CAAC,eAAe,CAAC,CAG1B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAM1D"}
1
+ {"version":3,"file":"http-data.d.ts","sourceRoot":"","sources":["../src/http-data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAK1D,OAAO,KAAK,EACV,UAAU,EAEV,YAAY,EACZ,eAAe,EACf,gBAAgB,EACjB,MAAM,cAAc,CAAC;AAEtB,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,KAC5B,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAgGpC;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,eAAe,EACpB,IAAI,CAAC,EAAE,UAAU,EACjB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC,CAM1B;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAwCzF"}
@@ -1 +1 @@
1
- {"version":3,"file":"http-error.d.ts","sourceRoot":"","sources":["../src/http-error.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,SAAU,SAAQ,KAAK;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;gBAER,OAAO,GAAE,gBAAqB;IAM1C,kBAAkB,CAAC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAIlF,MAAM,IAAI;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAQnE,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS;IAM3C,MAAM,CAAC,qBAAqB,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAQvE,MAAM,CAAC,cAAc,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAQhE,MAAM,CAAC,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAQnE,MAAM,CAAC,kBAAkB,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;CAOrE"}
1
+ {"version":3,"file":"http-error.d.ts","sourceRoot":"","sources":["../src/http-error.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,SAAU,SAAQ,KAAK;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,YAAY,OAAO,GAAE,gBAAqB,EAIzC;IAED,kBAAkB,CAAC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEjF;IAED,MAAM,IAAI;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAMlE;IAED,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAI1C;IAED,MAAM,CAAC,qBAAqB,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAMtE;IAED,MAAM,CAAC,cAAc,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAM/D;IAED,MAAM,CAAC,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAMlE;IAED,MAAM,CAAC,kBAAkB,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAMnE;CACF"}
@@ -19,9 +19,21 @@ export interface SerializedHttpResponse {
19
19
  }
20
20
  export interface SerializedHttpEnvelope<Options> {
21
21
  options: Options;
22
+ /**
23
+ * The body bytes. Must be *productive*: it has to yield or finish on its own,
24
+ * because a body neither stub is allowed to read (a GET/HEAD/OPTIONS request,
25
+ * a null-body-status response) is released with `discard`, and `discard` must
26
+ * await one `.next()` before `.return()` — a bare `.return()` is a no-op on a
27
+ * generator in suspended start and would leak the producer. So a `content`
28
+ * that blocks forever without yielding makes the stub block with it, where an
29
+ * earlier version returned immediately and leaked instead. Every transport in
30
+ * this repo satisfies this; a `content` that waits on a peer that may never
31
+ * send should carry its own timeout.
32
+ */
22
33
  content: AsyncIterable<Uint8Array>;
23
34
  }
24
35
  export type HttpHandler = (request: Request) => Response | Promise<Response>;
36
+ export declare const NULL_BODY_STATUSES: Set<number>;
25
37
  /**
26
38
  * Returns an HTTP handler that serializes a Request, hands the envelope to
27
39
  * `send` for transport, and deserializes the reply into a Response. Used on
@@ -1 +1 @@
1
- {"version":3,"file":"http-stubs.d.ts","sourceRoot":"","sources":["../src/http-stubs.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB,CAAC,OAAO;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAmB7E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,CACJ,QAAQ,EAAE,sBAAsB,CAAC,qBAAqB,CAAC,KACpD,OAAO,CAAC,sBAAsB,CAAC,sBAAsB,CAAC,GAAG,SAAS,CAAC,GACvE,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmC5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,GACnB,CACD,QAAQ,EACJ,sBAAsB,CAAC,qBAAqB,CAAC,GAC7C,OAAO,CAAC,sBAAsB,CAAC,qBAAqB,CAAC,CAAC,KACvD,OAAO,CAAC,sBAAsB,CAAC,sBAAsB,CAAC,CAAC,CAyC3D"}
1
+ {"version":3,"file":"http-stubs.d.ts","sourceRoot":"","sources":["../src/http-stubs.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB,CAAC,OAAO;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;OAUG;IACH,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAK7E,eAAO,MAAM,kBAAkB,aAAqC,CAAC;AAsBrE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,CACJ,QAAQ,EAAE,sBAAsB,CAAC,qBAAqB,CAAC,KACpD,OAAO,CAAC,sBAAsB,CAAC,sBAAsB,CAAC,GAAG,SAAS,CAAC,GACvE,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmD5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,GACnB,CACD,QAAQ,EACJ,sBAAsB,CAAC,qBAAqB,CAAC,GAC7C,OAAO,CAAC,sBAAsB,CAAC,qBAAqB,CAAC,CAAC,KACvD,OAAO,CAAC,sBAAsB,CAAC,sBAAsB,CAAC,CAAC,CA0D3D"}