@npy/fetch 0.1.2 → 0.1.3

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 (142) hide show
  1. package/README.md +143 -50
  2. package/bun.lock +68 -0
  3. package/examples/custom-proxy-client.ts +32 -0
  4. package/examples/http-client.ts +47 -0
  5. package/examples/proxy.ts +16 -0
  6. package/examples/simple.ts +15 -0
  7. package/package.json +25 -30
  8. package/src/_internal/consts.ts +3 -0
  9. package/{_internal/decode-stream-error.d.cts → src/_internal/decode-stream-error.ts} +7 -2
  10. package/src/_internal/error-mapping.ts +160 -0
  11. package/src/_internal/guards.ts +78 -0
  12. package/src/_internal/net.ts +173 -0
  13. package/src/_internal/promises.ts +22 -0
  14. package/src/_internal/streams.ts +52 -0
  15. package/src/_internal/symbols.ts +1 -0
  16. package/src/agent-pool.ts +157 -0
  17. package/src/agent.ts +408 -0
  18. package/src/body.ts +179 -0
  19. package/src/dialers/index.ts +3 -0
  20. package/src/dialers/proxy.ts +102 -0
  21. package/src/dialers/tcp.ts +162 -0
  22. package/src/encoding.ts +222 -0
  23. package/src/errors.ts +357 -0
  24. package/src/fetch.ts +626 -0
  25. package/src/http-client.ts +111 -0
  26. package/src/index.ts +14 -0
  27. package/src/io/_utils.ts +82 -0
  28. package/src/io/buf-writer.ts +183 -0
  29. package/src/io/io.ts +322 -0
  30. package/src/io/readers.ts +576 -0
  31. package/src/io/writers.ts +331 -0
  32. package/{types/agent.d.cts → src/types/agent.ts} +47 -21
  33. package/{types/dialer.d.cts → src/types/dialer.ts} +19 -9
  34. package/src/types/index.ts +2 -0
  35. package/tests/agent-pool.test.ts +111 -0
  36. package/tests/agent.test.ts +134 -0
  37. package/tests/body.test.ts +228 -0
  38. package/tests/errors.test.ts +152 -0
  39. package/tests/fetch.test.ts +421 -0
  40. package/tests/io-options.test.ts +127 -0
  41. package/tests/multipart.test.ts +348 -0
  42. package/tests/test-utils.ts +335 -0
  43. package/tsconfig.json +15 -0
  44. package/LICENSE +0 -21
  45. package/_internal/consts.cjs +0 -4
  46. package/_internal/consts.d.cts +0 -3
  47. package/_internal/consts.d.ts +0 -3
  48. package/_internal/consts.js +0 -4
  49. package/_internal/decode-stream-error.cjs +0 -18
  50. package/_internal/decode-stream-error.d.ts +0 -11
  51. package/_internal/decode-stream-error.js +0 -18
  52. package/_internal/error-mapping.cjs +0 -44
  53. package/_internal/error-mapping.d.cts +0 -15
  54. package/_internal/error-mapping.d.ts +0 -15
  55. package/_internal/error-mapping.js +0 -41
  56. package/_internal/guards.cjs +0 -23
  57. package/_internal/guards.d.cts +0 -15
  58. package/_internal/guards.d.ts +0 -15
  59. package/_internal/guards.js +0 -15
  60. package/_internal/net.cjs +0 -95
  61. package/_internal/net.d.cts +0 -11
  62. package/_internal/net.d.ts +0 -11
  63. package/_internal/net.js +0 -92
  64. package/_internal/promises.cjs +0 -18
  65. package/_internal/promises.d.cts +0 -1
  66. package/_internal/promises.d.ts +0 -1
  67. package/_internal/promises.js +0 -18
  68. package/_internal/streams.cjs +0 -37
  69. package/_internal/streams.d.cts +0 -21
  70. package/_internal/streams.d.ts +0 -21
  71. package/_internal/streams.js +0 -36
  72. package/_internal/symbols.cjs +0 -4
  73. package/_internal/symbols.d.cts +0 -1
  74. package/_internal/symbols.d.ts +0 -1
  75. package/_internal/symbols.js +0 -4
  76. package/_virtual/_rolldown/runtime.cjs +0 -23
  77. package/agent-pool.cjs +0 -96
  78. package/agent-pool.d.cts +0 -2
  79. package/agent-pool.d.ts +0 -2
  80. package/agent-pool.js +0 -95
  81. package/agent.cjs +0 -260
  82. package/agent.d.cts +0 -3
  83. package/agent.d.ts +0 -3
  84. package/agent.js +0 -259
  85. package/body.cjs +0 -105
  86. package/body.d.cts +0 -12
  87. package/body.d.ts +0 -12
  88. package/body.js +0 -102
  89. package/dialers/index.d.cts +0 -3
  90. package/dialers/index.d.ts +0 -3
  91. package/dialers/proxy.cjs +0 -56
  92. package/dialers/proxy.d.cts +0 -27
  93. package/dialers/proxy.d.ts +0 -27
  94. package/dialers/proxy.js +0 -55
  95. package/dialers/tcp.cjs +0 -92
  96. package/dialers/tcp.d.cts +0 -57
  97. package/dialers/tcp.d.ts +0 -57
  98. package/dialers/tcp.js +0 -89
  99. package/encoding.cjs +0 -114
  100. package/encoding.d.cts +0 -35
  101. package/encoding.d.ts +0 -35
  102. package/encoding.js +0 -110
  103. package/errors.cjs +0 -275
  104. package/errors.d.cts +0 -110
  105. package/errors.d.ts +0 -110
  106. package/errors.js +0 -259
  107. package/fetch.cjs +0 -353
  108. package/fetch.d.cts +0 -58
  109. package/fetch.d.ts +0 -58
  110. package/fetch.js +0 -350
  111. package/http-client.cjs +0 -75
  112. package/http-client.d.cts +0 -39
  113. package/http-client.d.ts +0 -39
  114. package/http-client.js +0 -75
  115. package/index.cjs +0 -49
  116. package/index.d.cts +0 -14
  117. package/index.d.ts +0 -14
  118. package/index.js +0 -11
  119. package/io/_utils.cjs +0 -56
  120. package/io/_utils.d.cts +0 -10
  121. package/io/_utils.d.ts +0 -10
  122. package/io/_utils.js +0 -51
  123. package/io/buf-writer.cjs +0 -149
  124. package/io/buf-writer.d.cts +0 -13
  125. package/io/buf-writer.d.ts +0 -13
  126. package/io/buf-writer.js +0 -148
  127. package/io/io.cjs +0 -199
  128. package/io/io.d.cts +0 -5
  129. package/io/io.d.ts +0 -5
  130. package/io/io.js +0 -198
  131. package/io/readers.cjs +0 -337
  132. package/io/readers.d.cts +0 -69
  133. package/io/readers.d.ts +0 -69
  134. package/io/readers.js +0 -333
  135. package/io/writers.cjs +0 -196
  136. package/io/writers.d.cts +0 -22
  137. package/io/writers.d.ts +0 -22
  138. package/io/writers.js +0 -195
  139. package/types/agent.d.ts +0 -72
  140. package/types/dialer.d.ts +0 -30
  141. package/types/index.d.cts +0 -2
  142. package/types/index.d.ts +0 -2
@@ -0,0 +1,111 @@
1
+ import { createAgentPool } from "./agent-pool";
2
+ import type { Agent, AgentPool } from "./types/agent";
3
+
4
+ /**
5
+ * Advanced HTTP client with per-origin pooling and explicit lifecycle control.
6
+ *
7
+ * @remarks
8
+ * Use this API when you want direct access to the library's richer error model and
9
+ * transport options instead of the fetch-like compatibility layer.
10
+ *
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const client = new HttpClient();
15
+ * const response = await client.send({
16
+ * url: "https://httpbin.org/anything",
17
+ * method: "GET",
18
+ * });
19
+ * await client.close();
20
+ * ```
21
+ */
22
+ export class HttpClient implements AsyncDisposable {
23
+ readonly #agentPools = new Map<string, AgentPool>();
24
+ readonly #agentPoolOptions: Readonly<HttpClient.Options>;
25
+ #closePromise?: Promise<void>;
26
+
27
+ constructor(options: HttpClient.Options = {}) {
28
+ this.#agentPoolOptions = { ...options };
29
+ }
30
+
31
+ get options(): Readonly<HttpClient.Options> {
32
+ return this.#agentPoolOptions;
33
+ }
34
+
35
+ async send(options: Agent.SendOptions): Promise<Response> {
36
+ const agentPool = this.#getOrCreateAgentPool(options.url);
37
+ return agentPool.send(options);
38
+ }
39
+
40
+ /**
41
+ * Closes all pooled connections owned by this client.
42
+ *
43
+ * @remarks
44
+ * After closing, future requests may recreate pools as needed.
45
+ */
46
+ async close(): Promise<void> {
47
+ if (this.#closePromise) {
48
+ return this.#closePromise;
49
+ }
50
+
51
+ const promise = (async () => {
52
+ const entries = Array.from(this.#agentPools.entries());
53
+
54
+ const results = await Promise.allSettled(
55
+ entries.map(async ([origin, agentPool]) => {
56
+ try {
57
+ await agentPool.close();
58
+ } finally {
59
+ this.#agentPools.delete(origin);
60
+ }
61
+ }),
62
+ );
63
+
64
+ const errors = results.flatMap((result) =>
65
+ result.status === "rejected" ? [result.reason] : [],
66
+ );
67
+
68
+ if (errors.length === 1) throw errors[0];
69
+
70
+ if (errors.length > 1) {
71
+ throw new AggregateError(
72
+ errors,
73
+ "Failed to close one or more agent pools",
74
+ );
75
+ }
76
+ })();
77
+
78
+ this.#closePromise = promise;
79
+
80
+ try {
81
+ await promise;
82
+ } finally {
83
+ if (this.#closePromise === promise) {
84
+ this.#closePromise = undefined;
85
+ }
86
+ }
87
+ }
88
+
89
+ async [Symbol.asyncDispose](): Promise<void> {
90
+ await this.close();
91
+ }
92
+
93
+ #getOrCreateAgentPool(url: string | URL): AgentPool {
94
+ const origin =
95
+ typeof url === "string" ? new URL(url).origin : url.origin;
96
+
97
+ let agentPool = this.#agentPools.get(origin);
98
+ if (!agentPool) {
99
+ agentPool = createAgentPool(origin, this.#agentPoolOptions);
100
+ this.#agentPools.set(origin, agentPool);
101
+ }
102
+
103
+ return agentPool;
104
+ }
105
+ }
106
+
107
+ export namespace HttpClient {
108
+ export interface Options extends AgentPool.Options {}
109
+ }
110
+
111
+ export interface HttpClientOptions extends HttpClient.Options {}
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type { FormDataPolyfill } from "./_internal/guards";
2
+ export type { WithSignal } from "./_internal/net";
3
+ export { connectTcp, connectTls, upgradeTls } from "./_internal/net";
4
+ export * from "./agent";
5
+ export * from "./agent-pool";
6
+ export * from "./body";
7
+ export * from "./dialers";
8
+ export * from "./encoding";
9
+ export * from "./errors";
10
+ export * from "./fetch";
11
+ export * from "./http-client";
12
+ export type { LineReader, Readers } from "./io/readers";
13
+ export type { Writers } from "./io/writers";
14
+ export * from "./types";
@@ -0,0 +1,82 @@
1
+ import bytes from "bytes";
2
+
3
+ type TransferEncodingInfo = {
4
+ has: boolean;
5
+ chunked: boolean;
6
+ codings: string[];
7
+ };
8
+
9
+ export function parseMaxBytes(value?: number | string): number | null {
10
+ if (value === undefined) return null;
11
+
12
+ if (typeof value === "number") {
13
+ if (!Number.isFinite(value) || value < 0) {
14
+ throw new Error(`invalid max size: ${String(value)}`);
15
+ }
16
+ return Math.floor(value);
17
+ }
18
+
19
+ const parsed = bytes.parse(value);
20
+ if (parsed == null || !Number.isFinite(parsed) || parsed < 0) {
21
+ throw new Error(`invalid max size: ${String(value)}`);
22
+ }
23
+
24
+ return parsed;
25
+ }
26
+
27
+ export function splitTokens(v: string | null): string[] {
28
+ if (!v) return [];
29
+ return v
30
+ .split(",")
31
+ .map((s) => s.trim().toLowerCase())
32
+ .filter(Boolean);
33
+ }
34
+
35
+ export function parseTransferEncoding(headers: Headers): TransferEncodingInfo {
36
+ const raw = headers.get("transfer-encoding");
37
+ const tks = splitTokens(raw);
38
+ if (tks.length === 0) return { has: false, chunked: false, codings: [] };
39
+
40
+ const chunkedIdx = tks.lastIndexOf("chunked");
41
+ const hasChunked = chunkedIdx !== -1;
42
+
43
+ if (hasChunked && chunkedIdx !== tks.length - 1) {
44
+ throw new Error(`Invalid transfer-encoding order: ${raw ?? ""}`);
45
+ }
46
+
47
+ if (hasChunked && tks.indexOf("chunked") !== chunkedIdx) {
48
+ throw new Error(
49
+ `Invalid transfer-encoding (duplicate chunked): ${raw ?? ""}`,
50
+ );
51
+ }
52
+
53
+ const codings = tks.filter((t) => t !== "chunked" && t !== "identity");
54
+
55
+ return { has: true, chunked: hasChunked, codings };
56
+ }
57
+
58
+ export function parseContentLength(headers: Headers): number | null {
59
+ const raw = headers.get("content-length");
60
+ if (!raw) return null;
61
+
62
+ const parts = raw
63
+ .split(",")
64
+ .map((s) => s.trim())
65
+ .filter(Boolean);
66
+ if (parts.length === 0) return null;
67
+
68
+ let value: number | null = null;
69
+
70
+ for (const p of parts) {
71
+ if (!/^\d+$/.test(p)) throw new Error(`Invalid content-length: ${raw}`);
72
+ const n = Number.parseInt(p, 10);
73
+ if (!Number.isFinite(n) || n < 0)
74
+ throw new Error(`Invalid content-length: ${raw}`);
75
+
76
+ if (value === null) value = n;
77
+ else if (value !== n)
78
+ throw new Error(`Conflicting content-length values: ${raw}`);
79
+ }
80
+
81
+ return value;
82
+ }
@@ -0,0 +1,183 @@
1
+ import type { ISyncWritable, IWritable } from "@fuman/io";
2
+ import { u8 } from "@fuman/utils";
3
+
4
+ const DEFAULT_BUF_SIZE = 4096;
5
+ const MIN_BUF_SIZE = 16;
6
+
7
+ export class BufWriter implements IWritable, ISyncWritable {
8
+ #buffer: Uint8Array;
9
+ #writable: IWritable;
10
+
11
+ #writePos = 0;
12
+ #error: Error | null = null;
13
+
14
+ #pending: Uint8Array[] = [];
15
+ #pendingBytes = 0;
16
+
17
+ #lastWrite: { buf: Uint8Array; size: number; internal: boolean } | null =
18
+ null;
19
+
20
+ constructor(writable: IWritable, size: number = DEFAULT_BUF_SIZE) {
21
+ if (size < MIN_BUF_SIZE) size = MIN_BUF_SIZE;
22
+ this.#buffer = u8.alloc(size);
23
+ this.#writable = writable;
24
+ }
25
+
26
+ get bufferSize(): number {
27
+ return this.#buffer.byteLength;
28
+ }
29
+
30
+ get buffered(): number {
31
+ return this.#pendingBytes + this.#writePos;
32
+ }
33
+
34
+ get available(): number {
35
+ return this.#buffer.byteLength - this.#writePos;
36
+ }
37
+
38
+ reset(writable: IWritable): void {
39
+ this.#error = null;
40
+ this.#writePos = 0;
41
+ this.#pending.length = 0;
42
+ this.#pendingBytes = 0;
43
+ this.#lastWrite = null;
44
+ this.#writable = writable;
45
+ }
46
+
47
+ writeSync(bytes: number): Uint8Array {
48
+ if (this.#error) throw this.#error;
49
+ if (bytes < 0) throw new RangeError("bytes must be >= 0");
50
+ if (this.#lastWrite)
51
+ throw new Error(
52
+ "disposeWriteSync must be called before the next writeSync",
53
+ );
54
+
55
+ if (bytes === 0) {
56
+ const empty = this.#buffer.subarray(this.#writePos, this.#writePos);
57
+ this.#lastWrite = { buf: empty, size: 0, internal: true };
58
+ return empty;
59
+ }
60
+
61
+ if (bytes > this.available && this.#writePos > 0) {
62
+ const copy = u8.allocWith(this.#buffer.subarray(0, this.#writePos));
63
+ this.#pending.push(copy);
64
+ this.#pendingBytes += copy.length;
65
+ this.#writePos = 0;
66
+ }
67
+
68
+ if (bytes <= this.#buffer.byteLength) {
69
+ if (bytes > this.available) {
70
+ const copy = u8.allocWith(
71
+ this.#buffer.subarray(0, this.#writePos),
72
+ );
73
+ this.#pending.push(copy);
74
+ this.#pendingBytes += copy.length;
75
+ this.#writePos = 0;
76
+ }
77
+
78
+ const start = this.#writePos;
79
+ const end = start + bytes;
80
+ const slice = this.#buffer.subarray(start, end);
81
+ this.#writePos = end;
82
+ this.#lastWrite = { buf: slice, size: bytes, internal: true };
83
+ return slice;
84
+ }
85
+
86
+ const chunk = u8.alloc(bytes);
87
+ this.#lastWrite = { buf: chunk, size: bytes, internal: false };
88
+ return chunk;
89
+ }
90
+
91
+ disposeWriteSync(written?: number): void {
92
+ const lw = this.#lastWrite;
93
+ if (!lw) return;
94
+
95
+ const w = written ?? lw.size;
96
+ if (w < 0 || w > lw.size) {
97
+ throw new RangeError(`written out of bounds: ${w} (0..${lw.size})`);
98
+ }
99
+
100
+ if (lw.internal) {
101
+ this.#writePos -= lw.size - w;
102
+ } else {
103
+ if (w > 0) {
104
+ const chunk = w === lw.size ? lw.buf : lw.buf.subarray(0, w);
105
+ this.#pending.push(chunk);
106
+ this.#pendingBytes += chunk.length;
107
+ }
108
+ }
109
+
110
+ this.#lastWrite = null;
111
+ }
112
+
113
+ async #flushPending(): Promise<void> {
114
+ if (this.#error) throw this.#error;
115
+ if (this.#lastWrite)
116
+ throw new Error(
117
+ "disposeWriteSync must be called before flush/write",
118
+ );
119
+
120
+ while (this.#pending.length > 0) {
121
+ const chunk = this.#pending[0];
122
+ try {
123
+ await this.#writable.write(chunk);
124
+ } catch (e) {
125
+ this.#error = e as Error;
126
+ throw e;
127
+ }
128
+ this.#pending.shift();
129
+ this.#pendingBytes -= chunk.length;
130
+ }
131
+ }
132
+
133
+ async flush(): Promise<void> {
134
+ await this.#flushPending();
135
+ if (this.#error) throw this.#error;
136
+ if (this.#writePos === 0) return;
137
+
138
+ try {
139
+ await this.#writable.write(
140
+ this.#buffer.subarray(0, this.#writePos),
141
+ );
142
+ } catch (e) {
143
+ this.#error = e as Error;
144
+ throw e;
145
+ }
146
+
147
+ this.#writePos = 0;
148
+ }
149
+
150
+ async write(bytes: Uint8Array): Promise<void> {
151
+ if (this.#error) throw this.#error;
152
+ if (!bytes.length) return;
153
+
154
+ await this.#flushPending();
155
+
156
+ if (this.#writePos === 0 && bytes.length >= this.#buffer.byteLength) {
157
+ try {
158
+ await this.#writable.write(bytes);
159
+ } catch (e) {
160
+ this.#error = e as Error;
161
+ throw e;
162
+ }
163
+ return;
164
+ }
165
+
166
+ let off = 0;
167
+ while (off < bytes.length) {
168
+ if (this.available === 0) {
169
+ await this.flush();
170
+ continue;
171
+ }
172
+
173
+ const toCopy = Math.min(this.available, bytes.length - off);
174
+ this.#buffer.set(bytes.subarray(off, off + toCopy), this.#writePos);
175
+ this.#writePos += toCopy;
176
+ off += toCopy;
177
+
178
+ if (this.#writePos === this.#buffer.byteLength) {
179
+ await this.flush();
180
+ }
181
+ }
182
+ }
183
+ }
package/src/io/io.ts ADDED
@@ -0,0 +1,322 @@
1
+ import type { IReadable } from "@fuman/io";
2
+ import type { IConnection } from "@fuman/net";
3
+ import { MaxBytesTransformStream } from "../_internal/streams";
4
+ import { decodeStream } from "../encoding";
5
+ import {
6
+ parseContentLength,
7
+ parseMaxBytes,
8
+ parseTransferEncoding,
9
+ splitTokens,
10
+ } from "./_utils";
11
+ import {
12
+ BodyReader,
13
+ ChunkedBodyReader,
14
+ LineReader,
15
+ type Readers,
16
+ } from "./readers";
17
+ import { createRequestWriter, type Writers } from "./writers";
18
+
19
+ function parseStatusLine(line: string) {
20
+ const m = /^HTTP\/(\d+)\.(\d+)\s+(\d{3})(?:\s+(.*))?$/.exec(line);
21
+ if (!m) throw new Error(`Invalid HTTP status line: ${line}`);
22
+
23
+ const major = Number(m[1]);
24
+ const minor = Number(m[2]);
25
+ const status = Number(m[3]);
26
+ if (
27
+ !Number.isFinite(major) ||
28
+ !Number.isFinite(minor) ||
29
+ !Number.isFinite(status)
30
+ ) {
31
+ throw new Error(`Invalid HTTP status line: ${line}`);
32
+ }
33
+
34
+ return { major, minor, status, statusText: m[4] ?? "" };
35
+ }
36
+
37
+ function toArrayBufferBytes(
38
+ bytes: Uint8Array<ArrayBufferLike>,
39
+ ): Uint8Array<ArrayBuffer> {
40
+ if (bytes.buffer instanceof ArrayBuffer) {
41
+ return bytes as Uint8Array<ArrayBuffer>;
42
+ }
43
+ return new Uint8Array(bytes);
44
+ }
45
+
46
+ function wrapStreamErrors(
47
+ source: ReadableStream<Uint8Array>,
48
+ mapError: (err: unknown) => unknown,
49
+ ): ReadableStream<Uint8Array> {
50
+ const reader = source.getReader();
51
+ let pending: Uint8Array | null = null;
52
+
53
+ return new ReadableStream({
54
+ type: "bytes",
55
+ async pull(controller) {
56
+ try {
57
+ const byob = controller.byobRequest;
58
+
59
+ if (byob?.view) {
60
+ const target = new Uint8Array(
61
+ byob.view.buffer,
62
+ byob.view.byteOffset,
63
+ byob.view.byteLength,
64
+ );
65
+
66
+ if (target.byteLength === 0) {
67
+ byob.respond(0);
68
+ return;
69
+ }
70
+
71
+ let written = 0;
72
+
73
+ if (pending && pending.byteLength > 0) {
74
+ const n = Math.min(
75
+ target.byteLength,
76
+ pending.byteLength,
77
+ );
78
+ target.set(pending.subarray(0, n), written);
79
+ written += n;
80
+ pending =
81
+ n === pending.byteLength
82
+ ? null
83
+ : pending.subarray(n);
84
+ }
85
+
86
+ while (written === 0) {
87
+ const { done, value } = await reader.read();
88
+
89
+ if (done) {
90
+ byob.respond(0);
91
+ controller.close();
92
+ return;
93
+ }
94
+
95
+ if (!value || value.byteLength === 0) continue;
96
+
97
+ const n = Math.min(
98
+ target.byteLength - written,
99
+ value.byteLength,
100
+ );
101
+ target.set(value.subarray(0, n), written);
102
+ written += n;
103
+
104
+ if (n < value.byteLength) {
105
+ pending = value.subarray(n);
106
+ }
107
+ }
108
+
109
+ byob.respond(written);
110
+ return;
111
+ }
112
+
113
+ if (pending && pending.byteLength > 0) {
114
+ const chunk = pending;
115
+ pending = null;
116
+ controller.enqueue(toArrayBufferBytes(chunk));
117
+ return;
118
+ }
119
+
120
+ const { done, value } = await reader.read();
121
+
122
+ if (done) {
123
+ controller.close();
124
+ return;
125
+ }
126
+
127
+ if (value && value.byteLength > 0) {
128
+ controller.enqueue(toArrayBufferBytes(value));
129
+ }
130
+ } catch (error) {
131
+ controller.error(mapError(error));
132
+ }
133
+ },
134
+
135
+ async cancel(reason) {
136
+ pending = null;
137
+ await reader.cancel(reason);
138
+ },
139
+ });
140
+ }
141
+
142
+ export async function readResponse(
143
+ conn: IConnection<unknown>,
144
+ options: Readers.Options & LineReader.ReadHeadersOptions = {},
145
+ shouldIgnoreBody: (status: number) => boolean,
146
+ onDone?: (reusable: boolean) => void,
147
+
148
+ mapBodyError?: (err: unknown) => unknown,
149
+ ): Promise<Response> {
150
+ const lr = new LineReader(conn, options);
151
+
152
+ const finalize = (() => {
153
+ let called = false;
154
+ return (reusable: boolean) => {
155
+ if (called) return;
156
+ called = true;
157
+ queueMicrotask(() => onDone?.(reusable));
158
+ };
159
+ })();
160
+
161
+ let statusLine!: {
162
+ major: number;
163
+ minor: number;
164
+ status: number;
165
+ statusText: string;
166
+ };
167
+ let headers!: Headers;
168
+
169
+ for (;;) {
170
+ const line = await lr.readLine();
171
+ if (line === null)
172
+ throw new Error("Unexpected EOF while reading status line");
173
+
174
+ const parsed = parseStatusLine(line);
175
+ const hdrs = await lr.readHeaders(options);
176
+
177
+ if (parsed.status >= 100 && parsed.status < 200) {
178
+ if (parsed.status === 101)
179
+ throw new Error("HTTP/1.1 protocol upgrades not supported");
180
+ continue;
181
+ }
182
+
183
+ statusLine = parsed;
184
+ headers = hdrs;
185
+ break;
186
+ }
187
+
188
+ const { major, minor, status, statusText } = statusLine;
189
+
190
+ const connectionTokens = splitTokens(headers.get("connection"));
191
+ const hasClose = connectionTokens.includes("close");
192
+ const hasKeepAlive = connectionTokens.includes("keep-alive");
193
+
194
+ const isHttp10 = major === 1 && minor === 0;
195
+ const keepAliveOk = !isHttp10 || hasKeepAlive;
196
+
197
+ const ignoreBody = shouldIgnoreBody(status);
198
+
199
+ const te = parseTransferEncoding(headers);
200
+
201
+ let chunked = false;
202
+ let contentLength: number | null = null;
203
+
204
+ if (!ignoreBody) {
205
+ if (te.has) {
206
+ chunked = te.chunked;
207
+ contentLength = null;
208
+ headers.delete("content-length");
209
+ } else {
210
+ chunked = false;
211
+ contentLength = parseContentLength(headers);
212
+ }
213
+ }
214
+
215
+ const bodyDelimited = ignoreBody || chunked || contentLength != null;
216
+ const reusable = keepAliveOk && !hasClose && bodyDelimited;
217
+
218
+ if (ignoreBody) {
219
+ finalize(reusable);
220
+ return new Response(null, { status, statusText, headers });
221
+ }
222
+
223
+ if (chunked) headers.delete("content-length");
224
+
225
+ const reader: IReadable = chunked
226
+ ? new ChunkedBodyReader(lr, options)
227
+ : new BodyReader(lr, contentLength, options);
228
+
229
+ const rawBody = new ReadableStream({
230
+ type: "bytes",
231
+ async pull(controller: ReadableByteStreamController) {
232
+ const byob = controller.byobRequest;
233
+ const view = byob?.view
234
+ ? new Uint8Array(
235
+ byob.view.buffer,
236
+ byob.view.byteOffset,
237
+ byob.view.byteLength,
238
+ )
239
+ : new Uint8Array(
240
+ new ArrayBuffer(options.readChunkSize ?? 16 * 1024),
241
+ );
242
+
243
+ try {
244
+ const n = await reader.read(view);
245
+ if (n === 0) {
246
+ byob?.respond(0);
247
+ controller.close();
248
+ finalize(reusable);
249
+ return;
250
+ }
251
+
252
+ if (byob) byob.respond(n);
253
+ else controller.enqueue(view.subarray(0, n));
254
+ } catch (err) {
255
+ controller.error(err);
256
+ finalize(false);
257
+ }
258
+ },
259
+ cancel() {
260
+ finalize(false);
261
+ },
262
+ });
263
+
264
+ let body: ReadableStream<Uint8Array> = rawBody;
265
+
266
+ try {
267
+ if (te.has && te.codings.length > 0) {
268
+ const decodedTe = decodeStream(
269
+ body,
270
+ te.codings,
271
+ ) as ReadableStream<Uint8Array>;
272
+ if (decodedTe !== body) {
273
+ if (te.chunked) headers.set("transfer-encoding", "chunked");
274
+ else headers.delete("transfer-encoding");
275
+
276
+ headers.delete("content-length");
277
+ }
278
+ body = decodedTe;
279
+ }
280
+
281
+ const decompress =
282
+ (options as Readers.DecompressionOptions).decompress !== false;
283
+ const contentEncoding = headers.get("content-encoding") ?? undefined;
284
+
285
+ if (decompress && contentEncoding) {
286
+ const decodedCe = decodeStream(
287
+ body,
288
+ contentEncoding,
289
+ ) as ReadableStream<Uint8Array>;
290
+ if (decodedCe !== body) {
291
+ headers.delete("content-encoding");
292
+ headers.delete("content-length");
293
+ }
294
+ body = decodedCe;
295
+ }
296
+
297
+ const maxDecoded = parseMaxBytes(
298
+ (options as Readers.SizeLimitOptions).maxDecodedBodySize,
299
+ );
300
+ if (maxDecoded != null) {
301
+ body = body.pipeThrough(new MaxBytesTransformStream(maxDecoded));
302
+ }
303
+
304
+ if (mapBodyError != null) {
305
+ body = wrapStreamErrors(body, mapBodyError);
306
+ }
307
+ } catch (err) {
308
+ finalize(false);
309
+ throw err;
310
+ }
311
+
312
+ return new Response(body, { status, statusText, headers });
313
+ }
314
+
315
+ export async function writeRequest(
316
+ conn: IConnection<unknown>,
317
+ req: Writers.Request,
318
+ options: Writers.Options = {},
319
+ ): Promise<void> {
320
+ const writer = createRequestWriter(conn, options);
321
+ await writer.write(req);
322
+ }