@statewalker/webrun-http-streams 0.3.2 → 0.4.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.
- package/README.md +49 -2
- package/dist/fetch.d.ts.map +1 -1
- package/dist/http-data.d.ts +20 -1
- package/dist/http-data.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -5
- package/package.json +1 -1
- package/src/fetch.ts +3 -1
- package/src/http-data.ts +69 -2
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -112,7 +112,8 @@ const { envelope, body } = await httpFetch(call, {
|
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
Signatures: `httpFetch(call, env, body?, { codec? })` and
|
|
115
|
-
`httpServe(handler, { codec? })
|
|
115
|
+
`httpServe(handler, { codec? })`; the handler is `(env, body, exchange) => result`, where
|
|
116
|
+
`exchange.signal` aborts when the caller abandons the exchange (see below). Over a real transport, `call` comes from an
|
|
116
117
|
adapter's `connect` and `httpServe(...)` goes to its `serve`.
|
|
117
118
|
|
|
118
119
|
A relative `url` works but does not survive the round trip verbatim. HTTP/1.1
|
|
@@ -137,6 +138,40 @@ Both take an optional last argument `{ codec }`. `fetchOverDuplex` plumbs
|
|
|
137
138
|
`request.signal` into body iteration, so aborting the signal ends the call. An
|
|
138
139
|
already-aborted signal rejects before anything is sent.
|
|
139
140
|
|
|
141
|
+
On the serving side, the handler's `request.signal` aborts when the exchange is abandoned before
|
|
142
|
+
it is over — the caller aborted its fetch, stopped reading, or its transport went away, or the
|
|
143
|
+
serving side tore the stream down (its timeout, a shutdown) — with `webrun-http-streams: the
|
|
144
|
+
exchange was abandoned before it was over` (`EXCHANGE_ABANDONED_MESSAGE`). A handler that waits
|
|
145
|
+
before it answers can stop then instead of holding on for nothing:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { serveFetchOverDuplex } from "@statewalker/webrun-http-streams";
|
|
149
|
+
|
|
150
|
+
// A long poll: answer in 30 s, or stop at once when the caller leaves.
|
|
151
|
+
const handler = serveFetchOverDuplex(async (request) => {
|
|
152
|
+
if (!request.signal.aborted) {
|
|
153
|
+
await new Promise<void>((resolve) => {
|
|
154
|
+
const timer = setTimeout(resolve, 30_000);
|
|
155
|
+
request.signal.addEventListener(
|
|
156
|
+
"abort",
|
|
157
|
+
() => {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
resolve();
|
|
160
|
+
},
|
|
161
|
+
{ once: true },
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return new Response(request.signal.aborted ? null : "news");
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Check `signal.aborted` before waiting: the exchange may already be abandoned when the handler
|
|
170
|
+
runs. The exchange is over once the response went out in full and the handler is done with the
|
|
171
|
+
request body; until then it can still be abandoned, so a handler that answers `202` first and
|
|
172
|
+
reads the upload afterwards sees an abort if the upload is cut. A completed exchange never aborts.
|
|
173
|
+
`newHttpServerStub` builds its requests without this signal.
|
|
174
|
+
|
|
140
175
|
### Site host: `DuplexSiteBuilder`
|
|
141
176
|
|
|
142
177
|
```ts
|
|
@@ -246,7 +281,7 @@ Check `err.name`, not `instanceof`; see
|
|
|
246
281
|
|
|
247
282
|
| Export | What it is |
|
|
248
283
|
| --- | --- |
|
|
249
|
-
| `httpFetch`, `httpServe` | Data layer. Types: `HttpDataHandler`, `HttpDataHandlerResult`, `HttpDataOptions`, `HttpFetchResult`. |
|
|
284
|
+
| `httpFetch`, `httpServe` | Data layer. Types: `HttpDataHandler`, `HttpDataHandlerResult`, `HttpDataOptions`, `HttpExchange`, `HttpFetchResult`; `EXCHANGE_ABANDONED_MESSAGE`. |
|
|
250
285
|
| `fetchOverDuplex`, `serveFetchOverDuplex` | Fetch layer. |
|
|
251
286
|
| `DuplexSiteBuilder`, `SiteHandler` | Site host over an adapter's `Serve<P>`. |
|
|
252
287
|
| `newHttpClientStub`, `newHttpServerStub` | Transport-agnostic stubs. Types: `HttpHandler`, `SerializedHttpEnvelope`, `SerializedHttpRequest`, `SerializedHttpResponse`. |
|
|
@@ -332,6 +367,18 @@ producer's `finally` does not run. On a transport that multiplexes calls with a
|
|
|
332
367
|
bounded stream table this holds one slot per such call. Closing it needs
|
|
333
368
|
`httpFetch` to expose the call for cancellation.
|
|
334
369
|
|
|
370
|
+
### How a handler that has not answered learns its caller left
|
|
371
|
+
|
|
372
|
+
A transport tells the serving side a caller is gone by calling `return()` on the handler's
|
|
373
|
+
output. On an async generator that call is queued behind the `next()` still pending inside it —
|
|
374
|
+
the `await handler(request)` — so it would arrive only once the handler answered, which a long
|
|
375
|
+
poll may never do. `httpServe` therefore hands the transport a generator whose `return()` and
|
|
376
|
+
`throw()` abort the exchange's signal at once, then forward as usual; one that already finished
|
|
377
|
+
aborts nothing. It relies on the transport calling `return()` without waiting for the pending
|
|
378
|
+
`next()`: `@statewalker/webrun-rpc`'s `serve` and `@statewalker/webrun-streams-libp2p`'s `serve` do.
|
|
379
|
+
A transport that only awaits `return()` after `next()` settles delivers the abandonment no
|
|
380
|
+
earlier than the answer.
|
|
381
|
+
|
|
335
382
|
### Hop-by-hop headers are dropped at the fetch layer
|
|
336
383
|
|
|
337
384
|
The codec surfaces every header verbatim. The fetch layer strips `connection`,
|
package/dist/fetch.d.ts.map
CHANGED
|
@@ -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;AAG1D,OAAO,EAAe,KAAK,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAsFzF;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA4DnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAChD,OAAO,GAAE,eAAoB,GAC5B,MAAM,
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAG1D,OAAO,EAAe,KAAK,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAsFzF;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA4DnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAChD,OAAO,GAAE,eAAoB,GAC5B,MAAM,CA0ER;AAED,wFAAwF;AACxF,eAAO,MAAM,uBAAuB,QAAS,CAAC"}
|
package/dist/http-data.d.ts
CHANGED
|
@@ -73,7 +73,26 @@ export interface HttpDataHandlerResult {
|
|
|
73
73
|
* conventional `HttpHandler` used by `webrun-http-browser` and `SiteBuilder`
|
|
74
74
|
* — see `http-stubs.ts` and `fetch.ts`.
|
|
75
75
|
*/
|
|
76
|
-
export type HttpDataHandler = (env: RequestEnvelope, body: AsyncIterable<Uint8Array
|
|
76
|
+
export type HttpDataHandler = (env: RequestEnvelope, body: AsyncIterable<Uint8Array>, exchange: HttpExchange) => Promise<HttpDataHandlerResult>;
|
|
77
|
+
/** What a handler is told about the exchange it serves. */
|
|
78
|
+
export interface HttpExchange {
|
|
79
|
+
/**
|
|
80
|
+
* Aborts when the exchange is abandoned before it is over: the caller gave
|
|
81
|
+
* up or its transport went away, or the serving side tore the stream down
|
|
82
|
+
* (its own timeout, a shutdown). A handler that waits before it answers (a
|
|
83
|
+
* long poll) can stop then. The exchange is over once the response went out
|
|
84
|
+
* in full and the handler is done with the request body: a handler that
|
|
85
|
+
* answers first and reads the upload afterwards still sees an abort if the
|
|
86
|
+
* upload is cut. A completed exchange never aborts.
|
|
87
|
+
*/
|
|
88
|
+
signal: AbortSignal;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The reason of `HttpExchange.signal` when the exchange is abandoned with no
|
|
92
|
+
* error of its own (`return()`); one abandoned by `throw(error)` aborts with
|
|
93
|
+
* that error.
|
|
94
|
+
*/
|
|
95
|
+
export declare const EXCHANGE_ABANDONED_MESSAGE = "webrun-http-streams: the exchange was abandoned before it was over";
|
|
77
96
|
/**
|
|
78
97
|
* Initiate an HTTP call over a `Duplex`. The caller's `call: Duplex` is
|
|
79
98
|
* obtained from any `webrun-streams-*` adapter's `connect`. Returns the
|
package/dist/http-data.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AAM1D,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;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChC;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,EAC/B,QAAQ,EAAE,YAAY,KACnB,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAEpC,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,MAAM,EAAE,WAAW,CAAC;CACrB;AAED;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,uEAC+B,CAAC;AAgGvE;;;;;;;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,CA0D1B;AAsBD,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAMtD;AA4CD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CA8DzF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { defaultCodec } from "./codec-default.js";
|
|
|
2
2
|
export { DuplexSiteBuilder, type SiteHandler } from "./duplex-site-builder.js";
|
|
3
3
|
export { decodeMessage, encodeMessage, jsonEnvelopeCodec, type RequestEnvelope, type ResponseEnvelope, } from "./envelope.js";
|
|
4
4
|
export { fetchOverDuplex, serveFetchOverDuplex } from "./fetch.js";
|
|
5
|
-
export { type HttpDataHandler, type HttpDataHandlerResult, type HttpDataOptions, type HttpFetchResult, httpFetch, httpServe, PEER_ERROR_HEADER, } from "./http-data.js";
|
|
5
|
+
export { EXCHANGE_ABANDONED_MESSAGE, type HttpDataHandler, type HttpDataHandlerResult, type HttpDataOptions, type HttpExchange, type HttpFetchResult, httpFetch, httpServe, PEER_ERROR_HEADER, } from "./http-data.js";
|
|
6
6
|
export { HttpError, type HttpErrorOptions } from "./http-error.js";
|
|
7
7
|
export { type HttpHandler, newHttpClientStub, newHttpServerStub, type SerializedHttpEnvelope, type SerializedHttpRequest, type SerializedHttpResponse, } from "./http-stubs.js";
|
|
8
8
|
export { type HttpCodecOptions, HttpParseError, httpCodec, newHttpCodec, } from "./http1/index.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EACL,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,SAAS,EACT,SAAS,EACT,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EACL,KAAK,WAAW,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,gBAAgB,EACrB,cAAc,EACd,SAAS,EACT,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EACL,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EACL,0BAA0B,EAC1B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,SAAS,EACT,SAAS,EACT,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EACL,KAAK,WAAW,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,gBAAgB,EACrB,cAAc,EACd,SAAS,EACT,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -800,6 +800,12 @@ function asyncDisposable(target, dispose) {
|
|
|
800
800
|
//#region src/http-data.ts
|
|
801
801
|
/** Carries the serialized JS error between two webrun peers. */
|
|
802
802
|
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
803
|
+
/**
|
|
804
|
+
* The reason of `HttpExchange.signal` when the exchange is abandoned with no
|
|
805
|
+
* error of its own (`return()`); one abandoned by `throw(error)` aborts with
|
|
806
|
+
* that error.
|
|
807
|
+
*/
|
|
808
|
+
const EXCHANGE_ABANDONED_MESSAGE = "webrun-http-streams: the exchange was abandoned before it was over";
|
|
803
809
|
/** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
|
|
804
810
|
const MAX_DETAIL_CHARS = 4096;
|
|
805
811
|
/**
|
|
@@ -980,7 +986,11 @@ async function drain(output) {
|
|
|
980
986
|
*/
|
|
981
987
|
function httpServe(handler, options = {}) {
|
|
982
988
|
const codec = options.codec ?? defaultCodec;
|
|
983
|
-
return
|
|
989
|
+
return (input) => {
|
|
990
|
+
const abandoned = new AbortController();
|
|
991
|
+
return abortingOnReturn(exchange(input, { signal: abandoned.signal }), (reason) => abandoned.abort(reason ?? /* @__PURE__ */ new Error("webrun-http-streams: the exchange was abandoned before it was over")));
|
|
992
|
+
};
|
|
993
|
+
async function* exchange(input, context) {
|
|
984
994
|
let decoded;
|
|
985
995
|
try {
|
|
986
996
|
decoded = await codec.decodeRequest(input);
|
|
@@ -992,7 +1002,7 @@ function httpServe(handler, options = {}) {
|
|
|
992
1002
|
const replyCodec = decoded.codec ?? codec;
|
|
993
1003
|
let result;
|
|
994
1004
|
try {
|
|
995
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
1005
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
996
1006
|
} catch (error) {
|
|
997
1007
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
998
1008
|
return;
|
|
@@ -1007,7 +1017,36 @@ function httpServe(handler, options = {}) {
|
|
|
1007
1017
|
await result.release?.();
|
|
1008
1018
|
} catch {}
|
|
1009
1019
|
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
1024
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
1025
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
1026
|
+
*/
|
|
1027
|
+
function abortingOnReturn(gen, abandon) {
|
|
1028
|
+
let finished = false;
|
|
1029
|
+
const settle = (r) => {
|
|
1030
|
+
if (r.done) finished = true;
|
|
1031
|
+
return r;
|
|
1032
|
+
};
|
|
1033
|
+
const wrapped = {
|
|
1034
|
+
next: (...args) => gen.next(...args).then(settle),
|
|
1035
|
+
return(value) {
|
|
1036
|
+
if (!finished) abandon();
|
|
1037
|
+
finished = true;
|
|
1038
|
+
return gen.return(value);
|
|
1039
|
+
},
|
|
1040
|
+
throw(error) {
|
|
1041
|
+
if (!finished) abandon(error);
|
|
1042
|
+
finished = true;
|
|
1043
|
+
return gen.throw(error);
|
|
1044
|
+
},
|
|
1045
|
+
[Symbol.asyncIterator]() {
|
|
1046
|
+
return wrapped;
|
|
1047
|
+
}
|
|
1010
1048
|
};
|
|
1049
|
+
return asyncDisposable(wrapped, () => wrapped.return(void 0));
|
|
1011
1050
|
}
|
|
1012
1051
|
//#endregion
|
|
1013
1052
|
//#region src/request-streams.ts
|
|
@@ -1285,10 +1324,11 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1285
1324
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
1286
1325
|
*/
|
|
1287
1326
|
function serveFetchOverDuplex(handler, options = {}) {
|
|
1288
|
-
return httpServe(async (env, body) => {
|
|
1327
|
+
return httpServe(async (env, body, exchange) => {
|
|
1289
1328
|
const reqInit = {
|
|
1290
1329
|
method: env.method,
|
|
1291
|
-
headers: forwardableHeaders(env.headers)
|
|
1330
|
+
headers: forwardableHeaders(env.headers),
|
|
1331
|
+
signal: exchange?.signal
|
|
1292
1332
|
};
|
|
1293
1333
|
let upload;
|
|
1294
1334
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
@@ -1584,4 +1624,4 @@ var HttpError = class HttpError extends Error {
|
|
|
1584
1624
|
}
|
|
1585
1625
|
};
|
|
1586
1626
|
//#endregion
|
|
1587
|
-
export { DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, decodeMessage, defaultCodec, encodeMessage, fetchOverDuplex, httpCodec, httpFetch, httpServe, jsonEnvelopeCodec, newHttpClientStub, newHttpCodec, newHttpServerStub, newSniffingCodec, serveFetchOverDuplex };
|
|
1627
|
+
export { DuplexSiteBuilder, EXCHANGE_ABANDONED_MESSAGE, HttpError, HttpParseError, PEER_ERROR_HEADER, decodeMessage, defaultCodec, encodeMessage, fetchOverDuplex, httpCodec, httpFetch, httpServe, jsonEnvelopeCodec, newHttpClientStub, newHttpCodec, newHttpServerStub, newSniffingCodec, serveFetchOverDuplex };
|
package/package.json
CHANGED
package/src/fetch.ts
CHANGED
|
@@ -166,10 +166,12 @@ export function serveFetchOverDuplex(
|
|
|
166
166
|
handler: (request: Request) => Promise<Response>,
|
|
167
167
|
options: HttpDataOptions = {},
|
|
168
168
|
): Duplex {
|
|
169
|
-
return httpServe(async (env, body) => {
|
|
169
|
+
return httpServe(async (env, body, exchange) => {
|
|
170
170
|
const reqInit: RequestInit = {
|
|
171
171
|
method: env.method,
|
|
172
172
|
headers: forwardableHeaders(env.headers),
|
|
173
|
+
// Aborts when the caller abandons the exchange (see `HttpExchange`).
|
|
174
|
+
signal: exchange?.signal,
|
|
173
175
|
};
|
|
174
176
|
let upload: ReturnType<typeof trackingEnd> | undefined;
|
|
175
177
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
package/src/http-data.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Duplex } from "@statewalker/webrun-streams";
|
|
2
2
|
import { deserializeError, serializeError } from "@statewalker/webrun-streams";
|
|
3
|
+
import { asyncDisposable } from "./async-dispose.js";
|
|
3
4
|
import { discard } from "./bytes.js";
|
|
4
5
|
import { defaultCodec } from "./codec-default.js";
|
|
5
6
|
import { HttpParseError } from "./http1/errors.js";
|
|
@@ -91,8 +92,31 @@ export interface HttpDataHandlerResult {
|
|
|
91
92
|
export type HttpDataHandler = (
|
|
92
93
|
env: RequestEnvelope,
|
|
93
94
|
body: AsyncIterable<Uint8Array>,
|
|
95
|
+
exchange: HttpExchange,
|
|
94
96
|
) => Promise<HttpDataHandlerResult>;
|
|
95
97
|
|
|
98
|
+
/** What a handler is told about the exchange it serves. */
|
|
99
|
+
export interface HttpExchange {
|
|
100
|
+
/**
|
|
101
|
+
* Aborts when the exchange is abandoned before it is over: the caller gave
|
|
102
|
+
* up or its transport went away, or the serving side tore the stream down
|
|
103
|
+
* (its own timeout, a shutdown). A handler that waits before it answers (a
|
|
104
|
+
* long poll) can stop then. The exchange is over once the response went out
|
|
105
|
+
* in full and the handler is done with the request body: a handler that
|
|
106
|
+
* answers first and reads the upload afterwards still sees an abort if the
|
|
107
|
+
* upload is cut. A completed exchange never aborts.
|
|
108
|
+
*/
|
|
109
|
+
signal: AbortSignal;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The reason of `HttpExchange.signal` when the exchange is abandoned with no
|
|
114
|
+
* error of its own (`return()`); one abandoned by `throw(error)` aborts with
|
|
115
|
+
* that error.
|
|
116
|
+
*/
|
|
117
|
+
export const EXCHANGE_ABANDONED_MESSAGE =
|
|
118
|
+
"webrun-http-streams: the exchange was abandoned before it was over";
|
|
119
|
+
|
|
96
120
|
/** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
|
|
97
121
|
const MAX_DETAIL_CHARS = 4096;
|
|
98
122
|
|
|
@@ -337,7 +361,16 @@ async function drain(output: AsyncGenerator<Uint8Array>): Promise<void> {
|
|
|
337
361
|
*/
|
|
338
362
|
export function httpServe(handler: HttpDataHandler, options: HttpDataOptions = {}): Duplex {
|
|
339
363
|
const codec = options.codec ?? defaultCodec;
|
|
340
|
-
return
|
|
364
|
+
return (input) => {
|
|
365
|
+
const abandoned = new AbortController();
|
|
366
|
+
return abortingOnReturn(exchange(input, { signal: abandoned.signal }), (reason) =>
|
|
367
|
+
abandoned.abort(reason ?? new Error(EXCHANGE_ABANDONED_MESSAGE)),
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
async function* exchange(
|
|
371
|
+
input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,
|
|
372
|
+
context: HttpExchange,
|
|
373
|
+
): AsyncGenerator<Uint8Array> {
|
|
341
374
|
// A refusal to parse the request must still produce a response. Left
|
|
342
375
|
// uncaught, the exception propagates out of this generator and NOTHING is
|
|
343
376
|
// written — a real peer waits, then sees the connection end with no status
|
|
@@ -366,7 +399,7 @@ export function httpServe(handler: HttpDataHandler, options: HttpDataOptions = {
|
|
|
366
399
|
const replyCodec = decoded.codec ?? codec;
|
|
367
400
|
let result: HttpDataHandlerResult;
|
|
368
401
|
try {
|
|
369
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
402
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
370
403
|
} catch (error) {
|
|
371
404
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
372
405
|
return;
|
|
@@ -387,5 +420,39 @@ export function httpServe(handler: HttpDataHandler, options: HttpDataOptions = {
|
|
|
387
420
|
}
|
|
388
421
|
}
|
|
389
422
|
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
428
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
429
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
430
|
+
*/
|
|
431
|
+
function abortingOnReturn<T>(
|
|
432
|
+
gen: AsyncGenerator<T>,
|
|
433
|
+
abandon: (reason?: unknown) => void,
|
|
434
|
+
): AsyncGenerator<T> {
|
|
435
|
+
let finished = false;
|
|
436
|
+
const settle = (r: IteratorResult<T>) => {
|
|
437
|
+
if (r.done) finished = true;
|
|
438
|
+
return r;
|
|
439
|
+
};
|
|
440
|
+
const wrapped: AsyncGenerator<T> = {
|
|
441
|
+
next: (...args: [] | [unknown]) => gen.next(...args).then(settle),
|
|
442
|
+
return(value) {
|
|
443
|
+
if (!finished) abandon();
|
|
444
|
+
finished = true;
|
|
445
|
+
return gen.return(value);
|
|
446
|
+
},
|
|
447
|
+
throw(error) {
|
|
448
|
+
if (!finished) abandon(error);
|
|
449
|
+
finished = true;
|
|
450
|
+
return gen.throw(error);
|
|
451
|
+
},
|
|
452
|
+
[Symbol.asyncIterator]() {
|
|
453
|
+
return wrapped;
|
|
454
|
+
},
|
|
390
455
|
};
|
|
456
|
+
// `await using` ends it as leaving a `for await` early does.
|
|
457
|
+
return asyncDisposable(wrapped, () => wrapped.return(undefined));
|
|
391
458
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,9 +9,11 @@ export {
|
|
|
9
9
|
} from "./envelope.js";
|
|
10
10
|
export { fetchOverDuplex, serveFetchOverDuplex } from "./fetch.js";
|
|
11
11
|
export {
|
|
12
|
+
EXCHANGE_ABANDONED_MESSAGE,
|
|
12
13
|
type HttpDataHandler,
|
|
13
14
|
type HttpDataHandlerResult,
|
|
14
15
|
type HttpDataOptions,
|
|
16
|
+
type HttpExchange,
|
|
15
17
|
type HttpFetchResult,
|
|
16
18
|
httpFetch,
|
|
17
19
|
httpServe,
|