@statewalker/webrun-http-streams 0.3.0 → 0.3.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.
- package/dist/async-dispose.d.ts +10 -0
- package/dist/async-dispose.d.ts.map +1 -0
- package/dist/fetch.d.ts +2 -0
- package/dist/fetch.d.ts.map +1 -1
- package/dist/http-data.d.ts +42 -4
- package/dist/http-data.d.ts.map +1 -1
- package/dist/http-stubs.d.ts.map +1 -1
- package/dist/http1/index.d.ts.map +1 -1
- package/dist/index.js +284 -45
- package/package.json +2 -2
- package/src/async-dispose.ts +22 -0
- package/src/bytes.ts +1 -1
- package/src/codec-default.ts +1 -1
- package/src/envelope.ts +2 -2
- package/src/fetch.ts +212 -27
- package/src/http-data.ts +143 -19
- package/src/http-stubs.ts +1 -1
- package/src/http1/chunked.ts +3 -3
- package/src/http1/decode.ts +1 -1
- package/src/http1/headers.ts +1 -1
- package/src/http1/index.ts +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
5
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
6
|
+
* declared types, which would require consumers to compile with the
|
|
7
|
+
* `esnext.disposable` lib.
|
|
8
|
+
*/
|
|
9
|
+
export declare function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T;
|
|
10
|
+
//# sourceMappingURL=async-dispose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-dispose.d.ts","sourceRoot":"","sources":["../src/async-dispose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAa/F"}
|
package/dist/fetch.d.ts
CHANGED
|
@@ -11,4 +11,6 @@ export declare function fetchOverDuplex(call: Duplex, request: Request, options?
|
|
|
11
11
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
12
12
|
*/
|
|
13
13
|
export declare function serveFetchOverDuplex(handler: (request: Request) => Promise<Response>, options?: HttpDataOptions): Duplex;
|
|
14
|
+
/** How long a request body may sit unread after the response before it is abandoned. */
|
|
15
|
+
export declare const DEFAULT_REQUEST_IDLE_MS = 30000;
|
|
14
16
|
//# sourceMappingURL=fetch.d.ts.map
|
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;
|
|
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,CAwER;AAED,wFAAwF;AACxF,eAAO,MAAM,uBAAuB,QAAS,CAAC"}
|
package/dist/http-data.d.ts
CHANGED
|
@@ -5,6 +5,24 @@ export declare const PEER_ERROR_HEADER = "x-webrun-error";
|
|
|
5
5
|
export type HttpDataOptions = {
|
|
6
6
|
/** Wire format. Defaults to `defaultCodec`: writes HTTP/1.1, accepts either. */
|
|
7
7
|
codec?: MessageCodec;
|
|
8
|
+
/**
|
|
9
|
+
* `httpFetch` only: aborting it while the response head is awaited releases
|
|
10
|
+
* the call and rejects with the signal's reason. (Once the head is in, the
|
|
11
|
+
* body is the caller's to leave.)
|
|
12
|
+
*/
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/**
|
|
15
|
+
* `serveFetchOverDuplex` only: once the response is out, how long the request
|
|
16
|
+
* body may go unread before it is abandoned and the exchange ends (default
|
|
17
|
+
* 30 s). A handler that answers first and reads the upload afterwards keeps
|
|
18
|
+
* it as long as it keeps reading — the time it spends on one chunk before it
|
|
19
|
+
* asks for the next counts as idle, so a handler that pauses longer between
|
|
20
|
+
* reads (one slow write per chunk) needs a larger bound; one that took the
|
|
21
|
+
* body and dropped it would otherwise keep the exchange — on a multiplexed
|
|
22
|
+
* transport, its port — open for good. `Infinity` waits for as long as it
|
|
23
|
+
* takes.
|
|
24
|
+
*/
|
|
25
|
+
requestIdleMs?: number;
|
|
8
26
|
};
|
|
9
27
|
export interface HttpFetchResult {
|
|
10
28
|
envelope: ResponseEnvelope;
|
|
@@ -17,17 +35,35 @@ export interface HttpFetchResult {
|
|
|
17
35
|
* custom transports) still type-check; `httpFetch` always sets it.
|
|
18
36
|
*/
|
|
19
37
|
release?(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* The caller is done with a response it took in full — read to its end, or
|
|
40
|
+
* one with no body to read (HEAD, 204, 304...): the call is read to its end
|
|
41
|
+
* in the background, so the peer ends it once its side is over (it may still
|
|
42
|
+
* be reading the request body). Reading `body` to its end does this by
|
|
43
|
+
* itself. Optional, like `release`; `httpFetch` always sets it.
|
|
44
|
+
*/
|
|
45
|
+
finish?(): void;
|
|
20
46
|
}
|
|
21
47
|
export interface HttpDataHandlerResult {
|
|
22
48
|
envelope: ResponseEnvelope;
|
|
23
49
|
body?: ByteSource;
|
|
24
50
|
/**
|
|
25
|
-
* Called when the caller abandons the response before its end
|
|
26
|
-
*
|
|
27
|
-
* protocol (`.return()` on a
|
|
28
|
-
*
|
|
51
|
+
* Called when the caller abandons the response before its end — never once
|
|
52
|
+
* the response went out in full. A body the codec never started pulling
|
|
53
|
+
* cannot be released through the iterator protocol (`.return()` on a
|
|
54
|
+
* generator in suspended start does nothing), so a producer that holds
|
|
55
|
+
* resources says here how to let them go.
|
|
29
56
|
*/
|
|
30
57
|
release?: () => unknown;
|
|
58
|
+
/**
|
|
59
|
+
* Awaited once the response went out in full: the exchange — and with it
|
|
60
|
+
* the call, and on a multiplexed transport its port — ends when it
|
|
61
|
+
* resolves. A handler that answers before it has read the request body, and
|
|
62
|
+
* goes on reading it, resolves this once it is done with it; until then the
|
|
63
|
+
* caller keeps sending. Left out, the exchange ends with the response, and
|
|
64
|
+
* the rest of the request is dropped.
|
|
65
|
+
*/
|
|
66
|
+
settled?: () => Promise<unknown>;
|
|
31
67
|
}
|
|
32
68
|
/**
|
|
33
69
|
* Low-level HTTP-over-Duplex handler. Takes the request envelope plus body
|
|
@@ -47,6 +83,8 @@ export type HttpDataHandler = (env: RequestEnvelope, body: AsyncIterable<Uint8Ar
|
|
|
47
83
|
* calls is the adapter's concern (native or `emulateMux`).
|
|
48
84
|
*/
|
|
49
85
|
export declare function httpFetch(call: Duplex, env: RequestEnvelope, body?: ByteSource, options?: HttpDataOptions): Promise<HttpFetchResult>;
|
|
86
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
87
|
+
export declare function abortReason(signal: AbortSignal): Error;
|
|
50
88
|
/**
|
|
51
89
|
* Wrap an HTTP handler as a `Duplex` so it can be registered with any
|
|
52
90
|
* `webrun-streams-*` adapter's `serve`.
|
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;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;
|
|
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;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,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,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,CAqDzF"}
|
package/dist/http-stubs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,aAAqD,CAAC;AAsBrF;;;;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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http1/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAKlD,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,YAAY,CAiBzE;AAED,eAAO,MAAM,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http1/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAKlD,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,YAAY,CAiBzE;AAED,eAAO,MAAM,SAAS,EAAE,YAA6C,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -139,8 +139,8 @@ var HttpParseError = class extends Error {
|
|
|
139
139
|
const NEWLINE = 10;
|
|
140
140
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
141
141
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
142
|
-
const encoder$1 = new TextEncoder();
|
|
143
|
-
const decoder = new TextDecoder();
|
|
142
|
+
const encoder$1 = /* @__PURE__ */ new TextEncoder();
|
|
143
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
144
144
|
/**
|
|
145
145
|
* Encode an HTTP envelope plus optional body as one continuous byte stream:
|
|
146
146
|
*
|
|
@@ -398,15 +398,15 @@ function resolveFraming(headers, version) {
|
|
|
398
398
|
}
|
|
399
399
|
//#endregion
|
|
400
400
|
//#region src/http1/chunked.ts
|
|
401
|
-
const CRLF = new Uint8Array([13, 10]);
|
|
402
|
-
const LAST_CHUNK = new Uint8Array([
|
|
401
|
+
const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
|
|
402
|
+
const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
403
403
|
48,
|
|
404
404
|
13,
|
|
405
405
|
10,
|
|
406
406
|
13,
|
|
407
407
|
10
|
|
408
408
|
]);
|
|
409
|
-
const encoder = new TextEncoder();
|
|
409
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
410
410
|
/**
|
|
411
411
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
412
412
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -717,7 +717,7 @@ function newHttpCodec(options = {}) {
|
|
|
717
717
|
decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
|
|
718
718
|
};
|
|
719
719
|
}
|
|
720
|
-
const httpCodec = newHttpCodec();
|
|
720
|
+
const httpCodec = /* @__PURE__ */ newHttpCodec();
|
|
721
721
|
//#endregion
|
|
722
722
|
//#region src/sniff.ts
|
|
723
723
|
/**
|
|
@@ -771,11 +771,32 @@ function newSniffingCodec(options) {
|
|
|
771
771
|
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
772
772
|
* no `codec` option is supplied.
|
|
773
773
|
*/
|
|
774
|
-
const defaultCodec = newSniffingCodec({
|
|
774
|
+
const defaultCodec = /* @__PURE__ */ newSniffingCodec({
|
|
775
775
|
write: httpCodec,
|
|
776
776
|
accept: [httpCodec, jsonEnvelopeCodec]
|
|
777
777
|
});
|
|
778
778
|
//#endregion
|
|
779
|
+
//#region src/async-dispose.ts
|
|
780
|
+
/**
|
|
781
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
782
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
783
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
784
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
785
|
+
* declared types, which would require consumers to compile with the
|
|
786
|
+
* `esnext.disposable` lib.
|
|
787
|
+
*/
|
|
788
|
+
function asyncDisposable(target, dispose) {
|
|
789
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
790
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
791
|
+
value: async () => {
|
|
792
|
+
await dispose();
|
|
793
|
+
},
|
|
794
|
+
configurable: true,
|
|
795
|
+
writable: true
|
|
796
|
+
});
|
|
797
|
+
return target;
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
779
800
|
//#region src/http-data.ts
|
|
780
801
|
/** Carries the serialized JS error between two webrun peers. */
|
|
781
802
|
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
@@ -854,41 +875,105 @@ async function throwIfPeerError(result, output) {
|
|
|
854
875
|
*/
|
|
855
876
|
async function httpFetch(call, env, body, options = {}) {
|
|
856
877
|
const codec = options.codec ?? defaultCodec;
|
|
878
|
+
const { signal } = options;
|
|
879
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
857
880
|
const output = call(codec.encodeRequest(env, body));
|
|
858
|
-
|
|
859
|
-
const release =
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
881
|
+
let released;
|
|
882
|
+
const release = () => {
|
|
883
|
+
released ??= (async () => {
|
|
884
|
+
try {
|
|
885
|
+
await output.return?.(void 0);
|
|
886
|
+
} catch {}
|
|
887
|
+
})();
|
|
888
|
+
return released;
|
|
889
|
+
};
|
|
890
|
+
let finished = false;
|
|
891
|
+
const finish = () => {
|
|
892
|
+
if (finished || released) return;
|
|
893
|
+
finished = true;
|
|
894
|
+
if (signal?.aborted) {
|
|
895
|
+
release();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const onAbort = () => void release();
|
|
899
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
900
|
+
drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
863
901
|
};
|
|
902
|
+
let decoded;
|
|
903
|
+
try {
|
|
904
|
+
decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
|
|
905
|
+
} catch (error) {
|
|
906
|
+
if (signal?.aborted) release();
|
|
907
|
+
else await release();
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
864
910
|
const result = {
|
|
865
911
|
envelope: decoded.envelope,
|
|
866
|
-
body: releasingOnExit(decoded.body, release),
|
|
867
|
-
release
|
|
912
|
+
body: releasingOnExit(decoded.body, finish, release),
|
|
913
|
+
release,
|
|
914
|
+
finish
|
|
868
915
|
};
|
|
869
916
|
await throwIfPeerError(result, output);
|
|
870
917
|
return result;
|
|
871
918
|
}
|
|
919
|
+
/** `promise`, rejected with the signal's reason if it aborts first. */
|
|
920
|
+
function untilAborted(promise, signal) {
|
|
921
|
+
if (!signal) return promise;
|
|
922
|
+
return new Promise((resolve, reject) => {
|
|
923
|
+
const onAbort = () => reject(abortReason(signal));
|
|
924
|
+
if (signal.aborted) return onAbort();
|
|
925
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
926
|
+
promise.then((value) => {
|
|
927
|
+
signal.removeEventListener("abort", onAbort);
|
|
928
|
+
resolve(value);
|
|
929
|
+
}, (error) => {
|
|
930
|
+
signal.removeEventListener("abort", onAbort);
|
|
931
|
+
reject(error);
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
936
|
+
function abortReason(signal) {
|
|
937
|
+
const reason = signal.reason;
|
|
938
|
+
if (reason instanceof Error) return reason;
|
|
939
|
+
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
940
|
+
err.name = "AbortError";
|
|
941
|
+
return err;
|
|
942
|
+
}
|
|
872
943
|
/**
|
|
873
|
-
* `body`,
|
|
944
|
+
* `body`, ending the call however it is left: read to its end, or left early.
|
|
874
945
|
*
|
|
875
946
|
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
876
|
-
* and
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
947
|
+
* and its ending never reaches the call's `output` (see `throwIfPeerError` for
|
|
948
|
+
* why the codec must not do that itself). A caller that stopped reading — an
|
|
949
|
+
* aborted `fetch`, a cancelled response stream — left the peer's producer
|
|
950
|
+
* parked forever; and one that read the body to its end left the call itself
|
|
951
|
+
* open, which on a multiplexed transport is one port per exchange until the
|
|
952
|
+
* multiplexer refuses to open more (`maxPorts`).
|
|
953
|
+
*
|
|
954
|
+
* Left early, the call is abandoned (`.return()`): the peer's producer is
|
|
955
|
+
* released. Read to its end, the call is not cut short but read to ITS end, in
|
|
956
|
+
* the background: the peer ends it once its side of the exchange is over,
|
|
957
|
+
* which may be later than the response — a handler that answered before it
|
|
958
|
+
* read the request body is still receiving it (`HttpDataHandlerResult.settled`),
|
|
959
|
+
* and abandoning the call would abort that upload.
|
|
882
960
|
*/
|
|
883
|
-
async function* releasingOnExit(body, release) {
|
|
961
|
+
async function* releasingOnExit(body, finish, release) {
|
|
884
962
|
let completed = false;
|
|
885
963
|
try {
|
|
886
964
|
yield* body;
|
|
887
965
|
completed = true;
|
|
888
966
|
} finally {
|
|
889
|
-
if (
|
|
967
|
+
if (completed) finish();
|
|
968
|
+
else await release();
|
|
890
969
|
}
|
|
891
970
|
}
|
|
971
|
+
/** Reads `output` to its end, discarding what is left, ignoring a failure. */
|
|
972
|
+
async function drain(output) {
|
|
973
|
+
try {
|
|
974
|
+
for (let r = await output.next(); !r.done; r = await output.next());
|
|
975
|
+
} catch {}
|
|
976
|
+
}
|
|
892
977
|
/**
|
|
893
978
|
* Wrap an HTTP handler as a `Duplex` so it can be registered with any
|
|
894
979
|
* `webrun-streams-*` adapter's `serve`.
|
|
@@ -916,6 +1001,7 @@ function httpServe(handler, options = {}) {
|
|
|
916
1001
|
try {
|
|
917
1002
|
yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
|
|
918
1003
|
completed = true;
|
|
1004
|
+
await result.settled?.();
|
|
919
1005
|
} finally {
|
|
920
1006
|
if (!completed) try {
|
|
921
1007
|
await result.release?.();
|
|
@@ -1178,17 +1264,21 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1178
1264
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
1179
1265
|
if (buffered.byteLength > 0) body = [buffered];
|
|
1180
1266
|
}
|
|
1181
|
-
const { envelope, body: respBody, release } = await httpFetch(call, env, body,
|
|
1267
|
+
const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
|
|
1268
|
+
...options,
|
|
1269
|
+
signal: request.signal ?? void 0
|
|
1270
|
+
});
|
|
1182
1271
|
const responseInit = {
|
|
1183
1272
|
status: envelope.status,
|
|
1184
1273
|
statusText: envelope.statusText,
|
|
1185
1274
|
headers: forwardableHeaders(envelope.headers)
|
|
1186
1275
|
};
|
|
1187
1276
|
if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
|
|
1188
|
-
|
|
1277
|
+
if (finish) finish();
|
|
1278
|
+
else await release?.();
|
|
1189
1279
|
return new Response(null, responseInit);
|
|
1190
1280
|
}
|
|
1191
|
-
return new Response(
|
|
1281
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1192
1282
|
}
|
|
1193
1283
|
/**
|
|
1194
1284
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
@@ -1200,13 +1290,21 @@ function serveFetchOverDuplex(handler, options = {}) {
|
|
|
1200
1290
|
method: env.method,
|
|
1201
1291
|
headers: forwardableHeaders(env.headers)
|
|
1202
1292
|
};
|
|
1293
|
+
let upload;
|
|
1203
1294
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
1204
1295
|
if (supportsRequestStreams()) {
|
|
1205
|
-
|
|
1296
|
+
upload = trackingEnd(body);
|
|
1297
|
+
reqInit.body = asyncIterableToReadable(upload.source);
|
|
1206
1298
|
reqInit.duplex = "half";
|
|
1207
1299
|
} else reqInit.body = await collectBytes(body);
|
|
1208
1300
|
}
|
|
1209
|
-
const
|
|
1301
|
+
const request = new Request(env.url, reqInit);
|
|
1302
|
+
const response = await handler(request);
|
|
1303
|
+
const settled = async () => {
|
|
1304
|
+
if (!upload) return;
|
|
1305
|
+
if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
|
|
1306
|
+
await upload.settle(options.requestIdleMs ?? 3e4);
|
|
1307
|
+
};
|
|
1210
1308
|
const respEnv = {
|
|
1211
1309
|
status: response.status,
|
|
1212
1310
|
statusText: response.statusText,
|
|
@@ -1216,32 +1314,173 @@ function serveFetchOverDuplex(handler, options = {}) {
|
|
|
1216
1314
|
await response.body?.cancel();
|
|
1217
1315
|
return {
|
|
1218
1316
|
envelope: respEnv,
|
|
1219
|
-
body: void 0
|
|
1317
|
+
body: void 0,
|
|
1318
|
+
settled
|
|
1220
1319
|
};
|
|
1221
1320
|
}
|
|
1222
1321
|
return {
|
|
1223
1322
|
envelope: respEnv,
|
|
1224
1323
|
body: response.body ? readableToAsyncIterable(response.body) : void 0,
|
|
1225
|
-
release: () => response.body?.cancel()
|
|
1324
|
+
release: () => response.body?.cancel(),
|
|
1325
|
+
settled
|
|
1226
1326
|
};
|
|
1227
1327
|
}, options);
|
|
1228
1328
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1329
|
+
/**
|
|
1330
|
+
* `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
|
|
1331
|
+
* failed, or abandoned (`return()` included before the first `next()`, which a
|
|
1332
|
+
* generator's `finally` would not see) — or once nobody has asked it for a
|
|
1333
|
+
* chunk for `idleMs`, in which case it is abandoned: later reads fail.
|
|
1334
|
+
*
|
|
1335
|
+
* Idle counts only while no `next()` is pending: a reader waiting for a slow
|
|
1336
|
+
* sender is still reading.
|
|
1337
|
+
*/
|
|
1338
|
+
function trackingEnd(source) {
|
|
1339
|
+
let end = () => {};
|
|
1340
|
+
let isEnded = false;
|
|
1341
|
+
const ended = new Promise((resolve) => {
|
|
1342
|
+
end = () => {
|
|
1343
|
+
isEnded = true;
|
|
1344
|
+
resolve();
|
|
1345
|
+
};
|
|
1346
|
+
});
|
|
1347
|
+
let abandoned;
|
|
1348
|
+
let pending = 0;
|
|
1349
|
+
let idleSince = Date.now();
|
|
1350
|
+
const it = source[Symbol.asyncIterator]();
|
|
1351
|
+
const abandon = (reason) => {
|
|
1352
|
+
abandoned ??= reason;
|
|
1353
|
+
end();
|
|
1354
|
+
Promise.resolve(it.return?.(void 0)).catch(() => {});
|
|
1355
|
+
};
|
|
1356
|
+
const tracked = {
|
|
1357
|
+
[Symbol.asyncIterator]() {
|
|
1358
|
+
return tracked;
|
|
1359
|
+
},
|
|
1360
|
+
async next() {
|
|
1361
|
+
if (abandoned) throw abandoned;
|
|
1362
|
+
pending++;
|
|
1363
|
+
try {
|
|
1364
|
+
const r = await it.next();
|
|
1365
|
+
if (r.done) end();
|
|
1366
|
+
return r;
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
end();
|
|
1369
|
+
throw error;
|
|
1370
|
+
} finally {
|
|
1371
|
+
if (--pending === 0) idleSince = Date.now();
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
async return(value) {
|
|
1375
|
+
end();
|
|
1376
|
+
try {
|
|
1377
|
+
await it.return?.(value);
|
|
1378
|
+
} catch {}
|
|
1379
|
+
return {
|
|
1380
|
+
done: true,
|
|
1381
|
+
value: void 0
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
asyncDisposable(tracked, () => tracked.return?.() ?? Promise.resolve());
|
|
1386
|
+
const settle = (idleMs) => {
|
|
1387
|
+
if (isEnded || !Number.isFinite(idleMs)) return ended;
|
|
1388
|
+
return new Promise((resolve) => {
|
|
1389
|
+
let timer;
|
|
1390
|
+
const check = () => {
|
|
1391
|
+
if (isEnded) return resolve();
|
|
1392
|
+
const idle = pending === 0 ? Date.now() - idleSince : 0;
|
|
1393
|
+
if (idle >= idleMs) {
|
|
1394
|
+
abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
|
|
1395
|
+
return resolve();
|
|
1396
|
+
}
|
|
1397
|
+
timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
|
|
1398
|
+
timer.unref?.();
|
|
1399
|
+
};
|
|
1400
|
+
ended.then(() => {
|
|
1401
|
+
clearTimeout(timer);
|
|
1402
|
+
resolve();
|
|
1403
|
+
});
|
|
1404
|
+
check();
|
|
1405
|
+
});
|
|
1406
|
+
};
|
|
1407
|
+
return {
|
|
1408
|
+
source: tracked,
|
|
1409
|
+
settle
|
|
1410
|
+
};
|
|
1238
1411
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1412
|
+
/**
|
|
1413
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1414
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1415
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1416
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1417
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1418
|
+
*/
|
|
1419
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1420
|
+
/**
|
|
1421
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1422
|
+
*
|
|
1423
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1424
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1425
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1426
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1427
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1428
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1429
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1430
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1431
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1432
|
+
*/
|
|
1433
|
+
function responseBody(body, signal, release) {
|
|
1434
|
+
const it = body[Symbol.asyncIterator]();
|
|
1435
|
+
let controller;
|
|
1436
|
+
let ended = false;
|
|
1437
|
+
let reading = false;
|
|
1438
|
+
const end = () => {
|
|
1439
|
+
ended = true;
|
|
1440
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1441
|
+
};
|
|
1442
|
+
const onAbort = () => {
|
|
1443
|
+
if (ended) return;
|
|
1444
|
+
end();
|
|
1445
|
+
try {
|
|
1446
|
+
controller.error(abortReason(signal));
|
|
1447
|
+
} catch {}
|
|
1448
|
+
release?.();
|
|
1449
|
+
};
|
|
1450
|
+
return new ReadableStream({
|
|
1451
|
+
start(c) {
|
|
1452
|
+
controller = c;
|
|
1453
|
+
if (signal?.aborted) onAbort();
|
|
1454
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1455
|
+
},
|
|
1456
|
+
async pull(c) {
|
|
1457
|
+
try {
|
|
1458
|
+
reading = true;
|
|
1459
|
+
const { done, value } = await it.next().finally(() => {
|
|
1460
|
+
reading = false;
|
|
1461
|
+
});
|
|
1462
|
+
if (ended) return;
|
|
1463
|
+
if (done) {
|
|
1464
|
+
end();
|
|
1465
|
+
c.close();
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
c.enqueue(value);
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
if (ended) return;
|
|
1471
|
+
end();
|
|
1472
|
+
c.error(err);
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
cancel() {
|
|
1476
|
+
end();
|
|
1477
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1478
|
+
if (!reading) return;
|
|
1479
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1480
|
+
grace.unref?.();
|
|
1481
|
+
returned.finally(() => clearTimeout(grace));
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1245
1484
|
}
|
|
1246
1485
|
//#endregion
|
|
1247
1486
|
//#region src/duplex-site-builder.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-http-streams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "HTTP request/response over a webrun-streams Duplex, encoded as HTTP/1.1, plus transport-agnostic fetch stubs",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"src"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@statewalker/webrun-streams": "^0.2.
|
|
33
|
+
"@statewalker/webrun-streams": "^0.2.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@biomejs/biome": "^2.5.15",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
5
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
6
|
+
* declared types, which would require consumers to compile with the
|
|
7
|
+
* `esnext.disposable` lib.
|
|
8
|
+
*/
|
|
9
|
+
export function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T {
|
|
10
|
+
// Read here, not at module level: a top-level read is code a bundler keeps.
|
|
11
|
+
const asyncDispose = (Symbol as { asyncDispose?: symbol }).asyncDispose;
|
|
12
|
+
if (asyncDispose && !(asyncDispose in target)) {
|
|
13
|
+
Object.defineProperty(target, asyncDispose, {
|
|
14
|
+
value: async () => {
|
|
15
|
+
await dispose();
|
|
16
|
+
},
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return target;
|
|
22
|
+
}
|
package/src/bytes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ByteSource } from "./message.js";
|
|
|
2
2
|
|
|
3
3
|
const CR = 0x0d;
|
|
4
4
|
const LF = 0x0a;
|
|
5
|
-
const EMPTY = new Uint8Array(0);
|
|
5
|
+
const EMPTY = /* @__PURE__ */ new Uint8Array(0);
|
|
6
6
|
|
|
7
7
|
export class ByteStreamError extends Error {
|
|
8
8
|
override readonly name = "ByteStreamError";
|
package/src/codec-default.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { newSniffingCodec } from "./sniff.js";
|
|
|
7
7
|
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
8
8
|
* no `codec` option is supplied.
|
|
9
9
|
*/
|
|
10
|
-
export const defaultCodec: MessageCodec = newSniffingCodec({
|
|
10
|
+
export const defaultCodec: MessageCodec = /* @__PURE__ */ newSniffingCodec({
|
|
11
11
|
write: httpCodec,
|
|
12
12
|
accept: [httpCodec, jsonEnvelopeCodec],
|
|
13
13
|
});
|
package/src/envelope.ts
CHANGED
|
@@ -15,8 +15,8 @@ const NEWLINE = 0x0a;
|
|
|
15
15
|
|
|
16
16
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
17
17
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
18
|
-
const encoder = new TextEncoder();
|
|
19
|
-
const decoder = new TextDecoder();
|
|
18
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
19
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Encode an HTTP envelope plus optional body as one continuous byte stream:
|
package/src/fetch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Duplex } from "@statewalker/webrun-streams";
|
|
2
|
+
import { asyncDisposable } from "./async-dispose.js";
|
|
2
3
|
import type { RequestEnvelope, ResponseEnvelope } from "./envelope.js";
|
|
3
|
-
import { type HttpDataOptions, httpFetch, httpServe } from "./http-data.js";
|
|
4
|
+
import { abortReason, type HttpDataOptions, httpFetch, httpServe } from "./http-data.js";
|
|
4
5
|
import { NULL_BODY_STATUSES } from "./http-stubs.js";
|
|
5
6
|
import type { ByteSource } from "./message.js";
|
|
6
7
|
import { collectBytes, supportsRequestStreams } from "./request-streams.js";
|
|
@@ -10,7 +11,7 @@ import { collectBytes, supportsRequestStreams } from "./request-streams.js";
|
|
|
10
11
|
* but they are meaningless to a `Request`/`Response`, and re-emitting them
|
|
11
12
|
* from a relay would corrupt its framing.
|
|
12
13
|
*/
|
|
13
|
-
const HOP_BY_HOP = new Set([
|
|
14
|
+
const HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
14
15
|
"connection",
|
|
15
16
|
"host",
|
|
16
17
|
"keep-alive",
|
|
@@ -126,24 +127,35 @@ export async function fetchOverDuplex(
|
|
|
126
127
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
127
128
|
if (buffered.byteLength > 0) body = [buffered];
|
|
128
129
|
}
|
|
129
|
-
const {
|
|
130
|
+
const {
|
|
131
|
+
envelope,
|
|
132
|
+
body: respBody,
|
|
133
|
+
release,
|
|
134
|
+
finish,
|
|
135
|
+
} = await httpFetch(call, env, body, {
|
|
136
|
+
...options,
|
|
137
|
+
signal: request.signal ?? undefined,
|
|
138
|
+
});
|
|
130
139
|
const responseInit: ResponseInit = {
|
|
131
140
|
status: envelope.status,
|
|
132
141
|
statusText: envelope.statusText,
|
|
133
142
|
headers: forwardableHeaders(envelope.headers),
|
|
134
143
|
};
|
|
135
144
|
// `new Response(body, { status })` throws for the null-body-status set, and
|
|
136
|
-
// HEAD/OPTIONS carry no body either, so return a bodyless Response
|
|
137
|
-
//
|
|
145
|
+
// HEAD/OPTIONS carry no body either, so return a bodyless Response. The
|
|
146
|
+
// caller has taken it in full: the call is finished as for any body read to
|
|
147
|
+
// its end — not abandoned, which would cut off an upload the handler is
|
|
148
|
+
// still reading.
|
|
138
149
|
if (
|
|
139
150
|
request.method === "HEAD" ||
|
|
140
151
|
request.method === "OPTIONS" ||
|
|
141
152
|
NULL_BODY_STATUSES.has(envelope.status)
|
|
142
153
|
) {
|
|
143
|
-
|
|
154
|
+
if (finish) finish();
|
|
155
|
+
else await release?.();
|
|
144
156
|
return new Response(null, responseInit);
|
|
145
157
|
}
|
|
146
|
-
return new Response(
|
|
158
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
/**
|
|
@@ -159,9 +171,11 @@ export function serveFetchOverDuplex(
|
|
|
159
171
|
method: env.method,
|
|
160
172
|
headers: forwardableHeaders(env.headers),
|
|
161
173
|
};
|
|
174
|
+
let upload: ReturnType<typeof trackingEnd> | undefined;
|
|
162
175
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
163
176
|
if (supportsRequestStreams()) {
|
|
164
|
-
|
|
177
|
+
upload = trackingEnd(body);
|
|
178
|
+
reqInit.body = asyncIterableToReadable(upload.source);
|
|
165
179
|
(reqInit as RequestInit & { duplex?: string }).duplex = "half";
|
|
166
180
|
} else {
|
|
167
181
|
// Firefox again, and this is the half that fails *silently*. The
|
|
@@ -180,6 +194,22 @@ export function serveFetchOverDuplex(
|
|
|
180
194
|
}
|
|
181
195
|
const request = new Request(env.url, reqInit);
|
|
182
196
|
const response = await handler(request);
|
|
197
|
+
// Once the response is out, the exchange ends when the handler is done
|
|
198
|
+
// with the request body too: a handler may answer first and read the
|
|
199
|
+
// upload afterwards (`202`, then store it). One it never took — not used,
|
|
200
|
+
// not locked — it will not read: cancel it, and the exchange ends now. One
|
|
201
|
+
// it took and stopped reading (a reader dropped, a clone nobody reads) is
|
|
202
|
+
// abandoned once nothing has asked it for a chunk for `requestIdleMs`.
|
|
203
|
+
const settled = async () => {
|
|
204
|
+
if (!upload) return;
|
|
205
|
+
if (!request.bodyUsed && !request.body?.locked) {
|
|
206
|
+
// Not awaited: the cancel of one branch of a tee (`request.clone()`)
|
|
207
|
+
// settles only once the other branch is cancelled too, and a clone
|
|
208
|
+
// nobody reads never is. The idle bound below ends that one.
|
|
209
|
+
void request.body?.cancel().catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
await upload.settle(options.requestIdleMs ?? DEFAULT_REQUEST_IDLE_MS);
|
|
212
|
+
};
|
|
183
213
|
const respEnv: ResponseEnvelope = {
|
|
184
214
|
status: response.status,
|
|
185
215
|
statusText: response.statusText,
|
|
@@ -195,7 +225,7 @@ export function serveFetchOverDuplex(
|
|
|
195
225
|
NULL_BODY_STATUSES.has(response.status)
|
|
196
226
|
) {
|
|
197
227
|
await response.body?.cancel();
|
|
198
|
-
return { envelope: respEnv, body: undefined };
|
|
228
|
+
return { envelope: respEnv, body: undefined, settled };
|
|
199
229
|
}
|
|
200
230
|
return {
|
|
201
231
|
envelope: respEnv,
|
|
@@ -204,28 +234,183 @@ export function serveFetchOverDuplex(
|
|
|
204
234
|
// through; cancel the stream itself. Once read, it is locked and this
|
|
205
235
|
// rejects, harmlessly: the reader's own cancellation has run.
|
|
206
236
|
release: () => response.body?.cancel(),
|
|
237
|
+
settled,
|
|
207
238
|
};
|
|
208
239
|
}, options);
|
|
209
240
|
}
|
|
210
241
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
/** How long a request body may sit unread after the response before it is abandoned. */
|
|
243
|
+
export const DEFAULT_REQUEST_IDLE_MS = 30_000;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
|
|
247
|
+
* failed, or abandoned (`return()` included before the first `next()`, which a
|
|
248
|
+
* generator's `finally` would not see) — or once nobody has asked it for a
|
|
249
|
+
* chunk for `idleMs`, in which case it is abandoned: later reads fail.
|
|
250
|
+
*
|
|
251
|
+
* Idle counts only while no `next()` is pending: a reader waiting for a slow
|
|
252
|
+
* sender is still reading.
|
|
253
|
+
*/
|
|
254
|
+
function trackingEnd(source: AsyncIterable<Uint8Array>): {
|
|
255
|
+
source: AsyncIterable<Uint8Array>;
|
|
256
|
+
settle(idleMs: number): Promise<void>;
|
|
257
|
+
} {
|
|
258
|
+
let end: () => void = () => {};
|
|
259
|
+
let isEnded = false;
|
|
260
|
+
const ended = new Promise<void>((resolve) => {
|
|
261
|
+
end = () => {
|
|
262
|
+
isEnded = true;
|
|
263
|
+
resolve();
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
let abandoned: Error | undefined;
|
|
267
|
+
let pending = 0;
|
|
268
|
+
let idleSince = Date.now();
|
|
269
|
+
const it = source[Symbol.asyncIterator]();
|
|
270
|
+
const abandon = (reason: Error) => {
|
|
271
|
+
abandoned ??= reason;
|
|
272
|
+
end();
|
|
273
|
+
void Promise.resolve(it.return?.(undefined)).catch(() => {});
|
|
274
|
+
};
|
|
275
|
+
const tracked: AsyncIterableIterator<Uint8Array> = {
|
|
276
|
+
[Symbol.asyncIterator]() {
|
|
277
|
+
return tracked;
|
|
278
|
+
},
|
|
279
|
+
async next() {
|
|
280
|
+
if (abandoned) throw abandoned;
|
|
281
|
+
pending++;
|
|
282
|
+
try {
|
|
283
|
+
const r = await it.next();
|
|
284
|
+
if (r.done) end();
|
|
285
|
+
return r;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
end();
|
|
288
|
+
throw error;
|
|
289
|
+
} finally {
|
|
290
|
+
if (--pending === 0) idleSince = Date.now();
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
async return(value?: unknown) {
|
|
294
|
+
end();
|
|
295
|
+
try {
|
|
296
|
+
await it.return?.(value);
|
|
297
|
+
} catch {
|
|
298
|
+
/* abandoned either way */
|
|
299
|
+
}
|
|
300
|
+
return { done: true, value: undefined };
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
asyncDisposable(tracked, () => tracked.return?.() ?? Promise.resolve());
|
|
304
|
+
const settle = (idleMs: number): Promise<void> => {
|
|
305
|
+
if (isEnded || !Number.isFinite(idleMs)) return ended;
|
|
306
|
+
return new Promise<void>((resolve) => {
|
|
307
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
308
|
+
const check = () => {
|
|
309
|
+
if (isEnded) return resolve();
|
|
310
|
+
const idle = pending === 0 ? Date.now() - idleSince : 0;
|
|
311
|
+
if (idle >= idleMs) {
|
|
312
|
+
abandon(
|
|
313
|
+
new Error(
|
|
314
|
+
`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`,
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
return resolve();
|
|
318
|
+
}
|
|
319
|
+
timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1_000)));
|
|
320
|
+
(timer as { unref?: () => void }).unref?.();
|
|
321
|
+
};
|
|
322
|
+
void ended.then(() => {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
resolve();
|
|
325
|
+
});
|
|
326
|
+
check();
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
return { source: tracked, settle };
|
|
223
330
|
}
|
|
224
331
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
332
|
+
/**
|
|
333
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
334
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
335
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
336
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
337
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
338
|
+
*/
|
|
339
|
+
const CANCEL_GRACE_MS = 1_000;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
343
|
+
*
|
|
344
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
345
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
346
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
347
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
348
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
349
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
350
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
351
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
352
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
353
|
+
*/
|
|
354
|
+
function responseBody(
|
|
355
|
+
body: AsyncIterable<Uint8Array>,
|
|
356
|
+
signal: AbortSignal | undefined,
|
|
357
|
+
release: (() => Promise<void>) | undefined,
|
|
358
|
+
): ReadableStream<Uint8Array> {
|
|
359
|
+
const it = body[Symbol.asyncIterator]();
|
|
360
|
+
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
361
|
+
let ended = false;
|
|
362
|
+
let reading = false;
|
|
363
|
+
const end = () => {
|
|
364
|
+
ended = true;
|
|
365
|
+
signal?.removeEventListener("abort", onAbort);
|
|
366
|
+
};
|
|
367
|
+
const onAbort = () => {
|
|
368
|
+
if (ended) return;
|
|
369
|
+
end();
|
|
370
|
+
try {
|
|
371
|
+
controller.error(abortReason(signal as AbortSignal));
|
|
372
|
+
} catch {
|
|
373
|
+
/* already closed or errored */
|
|
374
|
+
}
|
|
375
|
+
void release?.();
|
|
376
|
+
};
|
|
377
|
+
return new ReadableStream<Uint8Array>({
|
|
378
|
+
start(c) {
|
|
379
|
+
controller = c;
|
|
380
|
+
if (signal?.aborted) onAbort();
|
|
381
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
382
|
+
},
|
|
383
|
+
async pull(c) {
|
|
384
|
+
try {
|
|
385
|
+
reading = true;
|
|
386
|
+
const { done, value } = await it.next().finally(() => {
|
|
387
|
+
reading = false;
|
|
388
|
+
});
|
|
389
|
+
if (ended) return; // aborted or cancelled while this read waited
|
|
390
|
+
if (done) {
|
|
391
|
+
end();
|
|
392
|
+
c.close();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
c.enqueue(value);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (ended) return;
|
|
398
|
+
end();
|
|
399
|
+
c.error(err);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
cancel() {
|
|
403
|
+
end();
|
|
404
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
405
|
+
if (!reading) return; // `it` is at rest: its return abandons the call (releasingOnExit)
|
|
406
|
+
// A read is in flight, and `it.return()` is queued behind it: that read's answer decides.
|
|
407
|
+
// The body's end → the call is finished, not abandoned, so an upload the handler reads
|
|
408
|
+
// after answering goes on; a chunk → the return then abandons the call. A peer that never
|
|
409
|
+
// answers (sends nothing more, for good) would hold the call forever: abandon it after a
|
|
410
|
+
// grace, without waiting in `cancel()` either way.
|
|
411
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
412
|
+
(grace as { unref?: () => void }).unref?.();
|
|
413
|
+
void returned.finally(() => clearTimeout(grace));
|
|
414
|
+
},
|
|
415
|
+
});
|
|
231
416
|
}
|
package/src/http-data.ts
CHANGED
|
@@ -17,6 +17,24 @@ export const PEER_ERROR_HEADER = "x-webrun-error";
|
|
|
17
17
|
export type HttpDataOptions = {
|
|
18
18
|
/** Wire format. Defaults to `defaultCodec`: writes HTTP/1.1, accepts either. */
|
|
19
19
|
codec?: MessageCodec;
|
|
20
|
+
/**
|
|
21
|
+
* `httpFetch` only: aborting it while the response head is awaited releases
|
|
22
|
+
* the call and rejects with the signal's reason. (Once the head is in, the
|
|
23
|
+
* body is the caller's to leave.)
|
|
24
|
+
*/
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
/**
|
|
27
|
+
* `serveFetchOverDuplex` only: once the response is out, how long the request
|
|
28
|
+
* body may go unread before it is abandoned and the exchange ends (default
|
|
29
|
+
* 30 s). A handler that answers first and reads the upload afterwards keeps
|
|
30
|
+
* it as long as it keeps reading — the time it spends on one chunk before it
|
|
31
|
+
* asks for the next counts as idle, so a handler that pauses longer between
|
|
32
|
+
* reads (one slow write per chunk) needs a larger bound; one that took the
|
|
33
|
+
* body and dropped it would otherwise keep the exchange — on a multiplexed
|
|
34
|
+
* transport, its port — open for good. `Infinity` waits for as long as it
|
|
35
|
+
* takes.
|
|
36
|
+
*/
|
|
37
|
+
requestIdleMs?: number;
|
|
20
38
|
};
|
|
21
39
|
|
|
22
40
|
export interface HttpFetchResult {
|
|
@@ -30,18 +48,36 @@ export interface HttpFetchResult {
|
|
|
30
48
|
* custom transports) still type-check; `httpFetch` always sets it.
|
|
31
49
|
*/
|
|
32
50
|
release?(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* The caller is done with a response it took in full — read to its end, or
|
|
53
|
+
* one with no body to read (HEAD, 204, 304...): the call is read to its end
|
|
54
|
+
* in the background, so the peer ends it once its side is over (it may still
|
|
55
|
+
* be reading the request body). Reading `body` to its end does this by
|
|
56
|
+
* itself. Optional, like `release`; `httpFetch` always sets it.
|
|
57
|
+
*/
|
|
58
|
+
finish?(): void;
|
|
33
59
|
}
|
|
34
60
|
|
|
35
61
|
export interface HttpDataHandlerResult {
|
|
36
62
|
envelope: ResponseEnvelope;
|
|
37
63
|
body?: ByteSource;
|
|
38
64
|
/**
|
|
39
|
-
* Called when the caller abandons the response before its end
|
|
40
|
-
*
|
|
41
|
-
* protocol (`.return()` on a
|
|
42
|
-
*
|
|
65
|
+
* Called when the caller abandons the response before its end — never once
|
|
66
|
+
* the response went out in full. A body the codec never started pulling
|
|
67
|
+
* cannot be released through the iterator protocol (`.return()` on a
|
|
68
|
+
* generator in suspended start does nothing), so a producer that holds
|
|
69
|
+
* resources says here how to let them go.
|
|
43
70
|
*/
|
|
44
71
|
release?: () => unknown;
|
|
72
|
+
/**
|
|
73
|
+
* Awaited once the response went out in full: the exchange — and with it
|
|
74
|
+
* the call, and on a multiplexed transport its port — ends when it
|
|
75
|
+
* resolves. A handler that answers before it has read the request body, and
|
|
76
|
+
* goes on reading it, resolves this once it is done with it; until then the
|
|
77
|
+
* caller keeps sending. Left out, the exchange ends with the response, and
|
|
78
|
+
* the rest of the request is dropped.
|
|
79
|
+
*/
|
|
80
|
+
settled?: () => Promise<unknown>;
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
/**
|
|
@@ -166,37 +202,114 @@ export async function httpFetch(
|
|
|
166
202
|
options: HttpDataOptions = {},
|
|
167
203
|
): Promise<HttpFetchResult> {
|
|
168
204
|
const codec = options.codec ?? defaultCodec;
|
|
205
|
+
const { signal } = options;
|
|
206
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
169
207
|
const output = call(codec.encodeRequest(env, body));
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
// The call is one Duplex invocation, and only its own generator's ending
|
|
209
|
+
// ends it: on a multiplexed transport that `finally` is what frees the
|
|
210
|
+
// call's port. So every way out of an exchange ends it — the body read to
|
|
211
|
+
// its end (the call is read to its end too), left early, never started
|
|
212
|
+
// (bodyless), a peer error, a response that could not be decoded, an abort
|
|
213
|
+
// while waiting for the head (the call is abandoned).
|
|
214
|
+
let released: Promise<void> | undefined;
|
|
215
|
+
const release = () => {
|
|
216
|
+
released ??= (async () => {
|
|
217
|
+
try {
|
|
218
|
+
await output.return?.(undefined);
|
|
219
|
+
} catch {
|
|
220
|
+
/* the call is being abandoned; nothing to report */
|
|
221
|
+
}
|
|
222
|
+
})();
|
|
223
|
+
return released;
|
|
224
|
+
};
|
|
225
|
+
let finished = false;
|
|
226
|
+
const finish = () => {
|
|
227
|
+
if (finished || released) return;
|
|
228
|
+
finished = true;
|
|
229
|
+
// The peer ends the call; an abort of the request's signal abandons it
|
|
230
|
+
// instead (a peer that never ends a call, say).
|
|
231
|
+
if (signal?.aborted) {
|
|
232
|
+
void release();
|
|
233
|
+
return;
|
|
176
234
|
}
|
|
235
|
+
const onAbort = () => void release();
|
|
236
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
237
|
+
void drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
177
238
|
};
|
|
239
|
+
let decoded: Awaited<ReturnType<MessageCodec["decodeResponse"]>>;
|
|
240
|
+
try {
|
|
241
|
+
decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (signal?.aborted) {
|
|
244
|
+
// `.return()` on a generator with a `next()` in flight is queued behind
|
|
245
|
+
// it: the call ends once the peer answers that `next()`, which an abort
|
|
246
|
+
// must not wait for.
|
|
247
|
+
void release();
|
|
248
|
+
} else {
|
|
249
|
+
await release();
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
178
253
|
const result: HttpFetchResult = {
|
|
179
254
|
envelope: decoded.envelope,
|
|
180
|
-
body: releasingOnExit(decoded.body, release),
|
|
255
|
+
body: releasingOnExit(decoded.body, finish, release),
|
|
181
256
|
release,
|
|
257
|
+
finish,
|
|
182
258
|
};
|
|
183
259
|
await throwIfPeerError(result, output);
|
|
184
260
|
return result;
|
|
185
261
|
}
|
|
186
262
|
|
|
263
|
+
/** `promise`, rejected with the signal's reason if it aborts first. */
|
|
264
|
+
function untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
265
|
+
if (!signal) return promise;
|
|
266
|
+
return new Promise<T>((resolve, reject) => {
|
|
267
|
+
const onAbort = () => reject(abortReason(signal));
|
|
268
|
+
if (signal.aborted) return onAbort();
|
|
269
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
270
|
+
promise.then(
|
|
271
|
+
(value) => {
|
|
272
|
+
signal.removeEventListener("abort", onAbort);
|
|
273
|
+
resolve(value);
|
|
274
|
+
},
|
|
275
|
+
(error) => {
|
|
276
|
+
signal.removeEventListener("abort", onAbort);
|
|
277
|
+
reject(error);
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
284
|
+
export function abortReason(signal: AbortSignal): Error {
|
|
285
|
+
const reason = (signal as unknown as { reason?: unknown }).reason;
|
|
286
|
+
if (reason instanceof Error) return reason;
|
|
287
|
+
const err = new Error(reason === undefined ? "Aborted" : String(reason));
|
|
288
|
+
err.name = "AbortError";
|
|
289
|
+
return err;
|
|
290
|
+
}
|
|
291
|
+
|
|
187
292
|
/**
|
|
188
|
-
* `body`,
|
|
293
|
+
* `body`, ending the call however it is left: read to its end, or left early.
|
|
189
294
|
*
|
|
190
295
|
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
191
|
-
* and
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
296
|
+
* and its ending never reaches the call's `output` (see `throwIfPeerError` for
|
|
297
|
+
* why the codec must not do that itself). A caller that stopped reading — an
|
|
298
|
+
* aborted `fetch`, a cancelled response stream — left the peer's producer
|
|
299
|
+
* parked forever; and one that read the body to its end left the call itself
|
|
300
|
+
* open, which on a multiplexed transport is one port per exchange until the
|
|
301
|
+
* multiplexer refuses to open more (`maxPorts`).
|
|
302
|
+
*
|
|
303
|
+
* Left early, the call is abandoned (`.return()`): the peer's producer is
|
|
304
|
+
* released. Read to its end, the call is not cut short but read to ITS end, in
|
|
305
|
+
* the background: the peer ends it once its side of the exchange is over,
|
|
306
|
+
* which may be later than the response — a handler that answered before it
|
|
307
|
+
* read the request body is still receiving it (`HttpDataHandlerResult.settled`),
|
|
308
|
+
* and abandoning the call would abort that upload.
|
|
197
309
|
*/
|
|
198
310
|
async function* releasingOnExit(
|
|
199
311
|
body: AsyncIterable<Uint8Array>,
|
|
312
|
+
finish: () => void,
|
|
200
313
|
release: () => Promise<void>,
|
|
201
314
|
): AsyncGenerator<Uint8Array> {
|
|
202
315
|
let completed = false;
|
|
@@ -204,7 +317,17 @@ async function* releasingOnExit(
|
|
|
204
317
|
yield* body;
|
|
205
318
|
completed = true;
|
|
206
319
|
} finally {
|
|
207
|
-
if (
|
|
320
|
+
if (completed) finish();
|
|
321
|
+
else await release();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Reads `output` to its end, discarding what is left, ignoring a failure. */
|
|
326
|
+
async function drain(output: AsyncGenerator<Uint8Array>): Promise<void> {
|
|
327
|
+
try {
|
|
328
|
+
for (let r = await output.next(); !r.done; r = await output.next());
|
|
329
|
+
} catch {
|
|
330
|
+
/* the exchange is over for this side; how the peer ended it is its own */
|
|
208
331
|
}
|
|
209
332
|
}
|
|
210
333
|
|
|
@@ -254,6 +377,7 @@ export function httpServe(handler: HttpDataHandler, options: HttpDataOptions = {
|
|
|
254
377
|
method: decoded.envelope.method,
|
|
255
378
|
});
|
|
256
379
|
completed = true;
|
|
380
|
+
await result.settled?.();
|
|
257
381
|
} finally {
|
|
258
382
|
if (!completed) {
|
|
259
383
|
try {
|
package/src/http-stubs.ts
CHANGED
|
@@ -44,7 +44,7 @@ export type HttpHandler = (request: Request) => Response | Promise<Response>;
|
|
|
44
44
|
// Statuses that MUST NOT carry a body — `new Response(body, { status })` throws
|
|
45
45
|
// for these (per the Fetch spec's "null body status" set). Exported so
|
|
46
46
|
// `fetch.ts` mirrors this exact set rather than redefining it.
|
|
47
|
-
export const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
|
|
47
|
+
export const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
48
48
|
|
|
49
49
|
const REQUEST_FIELDS = [
|
|
50
50
|
"url",
|
package/src/http1/chunked.ts
CHANGED
|
@@ -4,9 +4,9 @@ import type { ByteSource } from "../message.js";
|
|
|
4
4
|
import { HttpParseError } from "./errors.js";
|
|
5
5
|
import { decodeLatin1 } from "./headers.js";
|
|
6
6
|
|
|
7
|
-
const CRLF = new Uint8Array([0x0d, 0x0a]);
|
|
8
|
-
const LAST_CHUNK = new Uint8Array([0x30, 0x0d, 0x0a, 0x0d, 0x0a]); // "0\r\n\r\n"
|
|
9
|
-
const encoder = new TextEncoder();
|
|
7
|
+
const CRLF = /* @__PURE__ */ new Uint8Array([0x0d, 0x0a]);
|
|
8
|
+
const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([0x30, 0x0d, 0x0a, 0x0d, 0x0a]); // "0\r\n\r\n"
|
|
9
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
package/src/http1/decode.ts
CHANGED
package/src/http1/headers.ts
CHANGED
package/src/http1/index.ts
CHANGED
|
@@ -36,5 +36,5 @@ export function newHttpCodec(options: HttpCodecOptions = {}): MessageCodec {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export const httpCodec: MessageCodec = newHttpCodec();
|
|
39
|
+
export const httpCodec: MessageCodec = /* @__PURE__ */ newHttpCodec();
|
|
40
40
|
export { HttpParseError } from "./errors.js";
|