@npy/fetch 0.1.1 → 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 (140) 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 +36 -25
  8. package/src/_internal/consts.ts +3 -0
  9. package/src/_internal/decode-stream-error.ts +16 -0
  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/src/types/agent.ts +98 -0
  33. package/src/types/{dialer.d.cts → dialer.ts} +22 -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.js +0 -4
  47. package/_internal/error-adapters.cjs +0 -146
  48. package/_internal/error-adapters.js +0 -142
  49. package/_internal/guards.cjs +0 -24
  50. package/_internal/guards.js +0 -17
  51. package/_internal/net.cjs +0 -95
  52. package/_internal/net.js +0 -92
  53. package/_internal/promises.cjs +0 -18
  54. package/_internal/promises.js +0 -18
  55. package/_internal/streams.cjs +0 -37
  56. package/_internal/streams.js +0 -36
  57. package/_virtual/_rolldown/runtime.cjs +0 -23
  58. package/agent-pool.cjs +0 -78
  59. package/agent-pool.js +0 -77
  60. package/agent.cjs +0 -257
  61. package/agent.js +0 -256
  62. package/body.cjs +0 -154
  63. package/body.js +0 -151
  64. package/dialers/proxy.cjs +0 -49
  65. package/dialers/proxy.js +0 -48
  66. package/dialers/tcp.cjs +0 -70
  67. package/dialers/tcp.js +0 -67
  68. package/encoding.cjs +0 -95
  69. package/encoding.js +0 -91
  70. package/errors.cjs +0 -275
  71. package/errors.js +0 -259
  72. package/fetch.cjs +0 -117
  73. package/fetch.js +0 -115
  74. package/http-client.cjs +0 -33
  75. package/http-client.js +0 -33
  76. package/index.cjs +0 -45
  77. package/index.d.cts +0 -1
  78. package/index.d.ts +0 -1
  79. package/index.js +0 -9
  80. package/io/_utils.cjs +0 -56
  81. package/io/_utils.js +0 -51
  82. package/io/buf-writer.cjs +0 -149
  83. package/io/buf-writer.js +0 -148
  84. package/io/io.cjs +0 -135
  85. package/io/io.js +0 -134
  86. package/io/readers.cjs +0 -377
  87. package/io/readers.js +0 -373
  88. package/io/writers.cjs +0 -191
  89. package/io/writers.js +0 -190
  90. package/src/_internal/consts.d.cts +0 -3
  91. package/src/_internal/consts.d.ts +0 -3
  92. package/src/_internal/error-adapters.d.cts +0 -22
  93. package/src/_internal/error-adapters.d.ts +0 -22
  94. package/src/_internal/guards.d.cts +0 -13
  95. package/src/_internal/guards.d.ts +0 -13
  96. package/src/_internal/net.d.cts +0 -12
  97. package/src/_internal/net.d.ts +0 -12
  98. package/src/_internal/promises.d.cts +0 -1
  99. package/src/_internal/promises.d.ts +0 -1
  100. package/src/_internal/streams.d.cts +0 -21
  101. package/src/_internal/streams.d.ts +0 -21
  102. package/src/agent-pool.d.cts +0 -2
  103. package/src/agent-pool.d.ts +0 -2
  104. package/src/agent.d.cts +0 -3
  105. package/src/agent.d.ts +0 -3
  106. package/src/body.d.cts +0 -23
  107. package/src/body.d.ts +0 -23
  108. package/src/dialers/index.d.cts +0 -3
  109. package/src/dialers/index.d.ts +0 -3
  110. package/src/dialers/proxy.d.cts +0 -19
  111. package/src/dialers/proxy.d.ts +0 -19
  112. package/src/dialers/tcp.d.cts +0 -36
  113. package/src/dialers/tcp.d.ts +0 -36
  114. package/src/encoding.d.cts +0 -24
  115. package/src/encoding.d.ts +0 -24
  116. package/src/errors.d.cts +0 -110
  117. package/src/errors.d.ts +0 -110
  118. package/src/fetch.d.cts +0 -36
  119. package/src/fetch.d.ts +0 -36
  120. package/src/http-client.d.cts +0 -23
  121. package/src/http-client.d.ts +0 -23
  122. package/src/index.d.cts +0 -7
  123. package/src/index.d.ts +0 -7
  124. package/src/io/_utils.d.cts +0 -10
  125. package/src/io/_utils.d.ts +0 -10
  126. package/src/io/buf-writer.d.cts +0 -13
  127. package/src/io/buf-writer.d.ts +0 -13
  128. package/src/io/io.d.cts +0 -5
  129. package/src/io/io.d.ts +0 -5
  130. package/src/io/readers.d.cts +0 -199
  131. package/src/io/readers.d.ts +0 -199
  132. package/src/io/writers.d.cts +0 -22
  133. package/src/io/writers.d.ts +0 -22
  134. package/src/types/agent.d.cts +0 -128
  135. package/src/types/agent.d.ts +0 -128
  136. package/src/types/dialer.d.ts +0 -27
  137. package/src/types/index.d.cts +0 -2
  138. package/src/types/index.d.ts +0 -2
  139. package/tests/test-utils.d.cts +0 -8
  140. package/tests/test-utils.d.ts +0 -8
@@ -0,0 +1,331 @@
1
+ import type { ISyncWritable, IWritable } from "@fuman/io";
2
+ import { Bytes, fumanReadableToWeb, write as ioWrite } from "@fuman/io";
3
+ import { nodeReadableToWeb } from "@fuman/node";
4
+ import { CRLF_STR } from "../_internal/consts";
5
+ import { isFumanReadable, isReadableStream } from "../_internal/guards";
6
+ import { bytesToStream } from "../_internal/streams";
7
+ import { type BodyInit, extractBody } from "../body";
8
+ import { createEncoders, encodeStream } from "../encoding";
9
+ import { parseContentLength, parseTransferEncoding } from "./_utils";
10
+ import { BufWriter } from "./buf-writer";
11
+ import { sanitizeHeaderValue } from "./readers";
12
+
13
+ type Destination = IWritable;
14
+ type ByteStream = ReadableStream<Uint8Array>;
15
+
16
+ type PreparedBody =
17
+ | { kind: "none" }
18
+ | { kind: "bytes"; bytes: Uint8Array; length: number }
19
+ | { kind: "stream"; stream: ByteStream; length: number | null };
20
+
21
+ type RequestHead = {
22
+ method: string;
23
+ target: string;
24
+ headers: Headers;
25
+ };
26
+
27
+ export namespace Writers {
28
+ export interface Options {
29
+ writeBufferSize?: number;
30
+ directWriteThreshold?: number;
31
+ coalesceBodyMaxBytes?: number;
32
+ }
33
+
34
+ export interface Request {
35
+ url: URL;
36
+ method: string;
37
+ headers?: Headers;
38
+ body?: BodyInit | null;
39
+ signal?: AbortSignal;
40
+ }
41
+
42
+ export interface Writer {
43
+ write(req: Request): Promise<void>;
44
+ }
45
+ }
46
+
47
+ function toRequestTarget(url: URL): string {
48
+ const pathname = url.pathname?.startsWith("/") ? url.pathname : "/";
49
+ return pathname + (url.search || "");
50
+ }
51
+
52
+ function encodeHead(into: ISyncWritable, head: RequestHead): void {
53
+ ioWrite.rawString(
54
+ into,
55
+ `${head.method.toUpperCase()} ${head.target} HTTP/1.1${CRLF_STR}`,
56
+ );
57
+
58
+ for (const [k, v] of head.headers) {
59
+ ioWrite.rawString(into, `${k}: ${sanitizeHeaderValue(v)}${CRLF_STR}`);
60
+ }
61
+
62
+ ioWrite.rawString(into, CRLF_STR);
63
+ }
64
+
65
+ function prepareBody(headers: Headers, init: BodyInit | null): PreparedBody {
66
+ const state = extractBody(init);
67
+
68
+ if (state.body != null && !headers.has("content-type")) {
69
+ headers.set(
70
+ "content-type",
71
+ state.contentType ?? "application/octet-stream",
72
+ );
73
+ }
74
+
75
+ const body = state.body;
76
+ if (body == null) return { kind: "none" };
77
+
78
+ if (body instanceof Uint8Array) {
79
+ return { kind: "bytes", bytes: body, length: body.byteLength };
80
+ }
81
+
82
+ if (isReadableStream(body)) {
83
+ return {
84
+ kind: "stream",
85
+ stream: body as ByteStream,
86
+ length: state.contentLength,
87
+ };
88
+ }
89
+
90
+ if (isFumanReadable(body)) {
91
+ return {
92
+ kind: "stream",
93
+ stream: fumanReadableToWeb(body) as ByteStream,
94
+ length: state.contentLength,
95
+ };
96
+ }
97
+
98
+ return {
99
+ kind: "stream",
100
+ stream: nodeReadableToWeb(body) as ByteStream,
101
+ length: state.contentLength,
102
+ };
103
+ }
104
+
105
+ function finalizeDelimitation(
106
+ headers: Headers,
107
+ body: PreparedBody,
108
+ ): { chunked: boolean } {
109
+ if (body.kind === "none") return { chunked: false };
110
+
111
+ const te = parseTransferEncoding(headers);
112
+ if (te.has) {
113
+ headers.delete("content-length");
114
+
115
+ if (!te.chunked) {
116
+ const tokens = [...te.codings, "chunked"].filter(Boolean);
117
+ headers.set("transfer-encoding", tokens.join(", "));
118
+ }
119
+
120
+ return { chunked: true };
121
+ }
122
+
123
+ const knownLength =
124
+ body.kind === "bytes" ? body.length : (body.length ?? null);
125
+ if (knownLength != null) {
126
+ const existing = parseContentLength(headers);
127
+ if (existing != null && existing !== knownLength) {
128
+ throw new Error(
129
+ `Conflicting content-length: header=${existing} body=${knownLength}`,
130
+ );
131
+ }
132
+ if (existing == null) {
133
+ headers.set("content-length", String(knownLength));
134
+ }
135
+ return { chunked: false };
136
+ }
137
+
138
+ const existing = parseContentLength(headers);
139
+ if (existing != null) return { chunked: false };
140
+
141
+ headers.set("transfer-encoding", "chunked");
142
+ headers.delete("content-length");
143
+ return { chunked: true };
144
+ }
145
+
146
+ function createBufferedConnWriter(dst: Destination, opts: Writers.Options) {
147
+ const bufferSize = opts.writeBufferSize ?? 16 * 1024;
148
+ const directWriteThreshold = opts.directWriteThreshold ?? 64 * 1024;
149
+ const bufWriter = new BufWriter(dst, bufferSize);
150
+
151
+ const flush = async (): Promise<void> => {
152
+ await bufWriter.flush();
153
+ };
154
+
155
+ const writeBytes = async (bytes: Uint8Array): Promise<void> => {
156
+ if (bytes.length === 0) return;
157
+
158
+ if (bytes.length >= directWriteThreshold) {
159
+ await bufWriter.flush();
160
+ await dst.write(bytes);
161
+ return;
162
+ }
163
+
164
+ await bufWriter.write(bytes);
165
+ };
166
+
167
+ const writeRawString = async (str: string): Promise<void> => {
168
+ if (str.length === 0) return;
169
+ ioWrite.rawString(bufWriter, str);
170
+ };
171
+
172
+ return { flush, writeBytes, writeRawString, directWriteThreshold };
173
+ }
174
+
175
+ async function writeBody(
176
+ dst: Destination,
177
+ body: Exclude<PreparedBody, { kind: "none" }>,
178
+ chunked: boolean,
179
+ opts: Writers.Options,
180
+ signal?: AbortSignal,
181
+ ): Promise<void> {
182
+ const bw = createBufferedConnWriter(dst, opts);
183
+
184
+ const writeChunk = async (chunk: Uint8Array) => {
185
+ if (signal?.aborted)
186
+ throw signal.reason ?? new Error("Request aborted");
187
+
188
+ if (!chunked) {
189
+ await bw.writeBytes(chunk);
190
+ return;
191
+ }
192
+
193
+ if (chunk.length === 0) return;
194
+
195
+ await bw.writeRawString(chunk.length.toString(16));
196
+ await bw.writeRawString(CRLF_STR);
197
+ await bw.writeBytes(chunk);
198
+ await bw.writeRawString(CRLF_STR);
199
+ };
200
+
201
+ if (body.kind === "bytes") {
202
+ await writeChunk(body.bytes);
203
+ } else {
204
+ for await (const chunk of body.stream) {
205
+ await writeChunk(chunk);
206
+ }
207
+ }
208
+
209
+ if (chunked) {
210
+ await bw.writeRawString(`0${CRLF_STR}${CRLF_STR}`);
211
+ }
212
+
213
+ await bw.flush();
214
+ }
215
+
216
+ async function writeCoalesced(
217
+ dst: Destination,
218
+ scratch: Bytes,
219
+ head: RequestHead,
220
+ bodyBytes: Uint8Array,
221
+ chunked: boolean,
222
+ ): Promise<void> {
223
+ scratch.reset();
224
+ encodeHead(scratch, head);
225
+
226
+ if (!chunked) {
227
+ ioWrite.bytes(scratch, bodyBytes);
228
+ await dst.write(scratch.result());
229
+ scratch.reset();
230
+ return;
231
+ }
232
+
233
+ ioWrite.rawString(scratch, bodyBytes.length.toString(16));
234
+ ioWrite.rawString(scratch, CRLF_STR);
235
+ ioWrite.bytes(scratch, bodyBytes);
236
+ ioWrite.rawString(scratch, `${CRLF_STR}0${CRLF_STR}${CRLF_STR}`);
237
+
238
+ await dst.write(scratch.result());
239
+ scratch.reset();
240
+ }
241
+
242
+ export function createRequestWriter(
243
+ dst: Destination,
244
+ opts: Writers.Options = {},
245
+ ): Writers.Writer {
246
+ const scratch = Bytes.alloc(opts.writeBufferSize ?? 16 * 1024);
247
+
248
+ const write = async (req: Writers.Request): Promise<void> => {
249
+ if (req.signal?.aborted)
250
+ throw req.signal.reason ?? new Error("Request aborted");
251
+
252
+ const method = req.method.toUpperCase();
253
+ const headers = req.headers ? new Headers(req.headers) : new Headers();
254
+ const url = req.url;
255
+
256
+ if (!headers.has("host")) headers.set("host", url.host);
257
+ if (!headers.has("date")) headers.set("date", new Date().toUTCString());
258
+
259
+ const target = toRequestTarget(url);
260
+
261
+ let body = prepareBody(headers, req.body ?? null);
262
+
263
+ const ceRaw = headers.get("content-encoding") ?? undefined;
264
+ const ceEncoders = createEncoders(ceRaw);
265
+
266
+ if (body.kind !== "none" && ceEncoders.length > 0) {
267
+ const stream =
268
+ body.kind === "stream"
269
+ ? body.stream
270
+ : bytesToStream(body.bytes);
271
+
272
+ body = {
273
+ kind: "stream",
274
+ stream: encodeStream(stream, ceRaw) as ByteStream,
275
+ length: null,
276
+ };
277
+
278
+ headers.delete("content-length");
279
+ }
280
+
281
+ const teInfo = parseTransferEncoding(headers);
282
+ if (body.kind !== "none" && teInfo.has && teInfo.codings.length > 0) {
283
+ const stream =
284
+ body.kind === "stream"
285
+ ? body.stream
286
+ : bytesToStream(body.bytes);
287
+
288
+ body = {
289
+ kind: "stream",
290
+ stream: encodeStream(stream, teInfo.codings) as ByteStream,
291
+ length: null,
292
+ };
293
+
294
+ headers.delete("content-length");
295
+ }
296
+
297
+ const { chunked } = finalizeDelimitation(headers, body);
298
+
299
+ const head: RequestHead = {
300
+ method,
301
+ target,
302
+ headers,
303
+ };
304
+
305
+ if (body.kind === "bytes") {
306
+ const max = opts.coalesceBodyMaxBytes ?? 64 * 1024;
307
+ if (body.bytes.length <= max) {
308
+ await writeCoalesced(dst, scratch, head, body.bytes, chunked);
309
+ return;
310
+ }
311
+ }
312
+
313
+ scratch.reset();
314
+ encodeHead(scratch, head);
315
+ await dst.write(scratch.result());
316
+ scratch.reset();
317
+
318
+ if (body.kind === "none") return;
319
+
320
+ if (body.kind === "bytes" && !chunked) {
321
+ if (req.signal?.aborted)
322
+ throw req.signal.reason ?? new Error("Request aborted");
323
+ await dst.write(body.bytes);
324
+ return;
325
+ }
326
+
327
+ await writeBody(dst, body, chunked, opts, req.signal);
328
+ };
329
+
330
+ return { write };
331
+ }
@@ -0,0 +1,98 @@
1
+ import type { BodyInit } from "../body";
2
+ import type { LineReader, Readers } from "../io/readers";
3
+ import type { Writers } from "../io/writers";
4
+ import type { Dialer } from "./dialer";
5
+
6
+ export interface Agent {
7
+ [Symbol.dispose](): void;
8
+ close(): void;
9
+
10
+ readonly hostname: string;
11
+ readonly port: number;
12
+
13
+ /**
14
+ * Sends a single HTTP request and returns the raw {@link Response}.
15
+ *
16
+ * @remarks
17
+ * The returned response body preserves the advanced error mapping of this library.
18
+ * Limits configured through reader options are enforced while the body is consumed.
19
+ */
20
+ send(options: Agent.SendOptions): Promise<Response>;
21
+
22
+ whenIdle(): Promise<void>;
23
+
24
+ readonly isIdle: boolean;
25
+ readonly lastUsed: number;
26
+ }
27
+
28
+ export namespace Agent {
29
+ export interface ConnectOptions {
30
+ timeout?: number;
31
+
32
+ keepAlive?: boolean | null;
33
+
34
+ noDelay?: boolean;
35
+ }
36
+
37
+ export type ReaderOptions = Readers.Options & LineReader.ReadHeadersOptions;
38
+
39
+ export type WriterOptions = Writers.Options;
40
+
41
+ export interface IOOptions {
42
+ reader?: ReaderOptions;
43
+ writer?: WriterOptions;
44
+ }
45
+
46
+ export interface Options {
47
+ connect?: ConnectOptions;
48
+ io?: IOOptions;
49
+ }
50
+
51
+ export interface SendOptions {
52
+ url: string | URL;
53
+
54
+ method: string;
55
+
56
+ headers?: Headers;
57
+
58
+ body?: BodyInit | null;
59
+ signal?: AbortSignal;
60
+ }
61
+ }
62
+
63
+ export interface AgentPool {
64
+ [Symbol.asyncDispose](): Promise<void>;
65
+ close(): Promise<void>;
66
+
67
+ readonly hostname: string;
68
+ readonly port: number;
69
+
70
+ send(options: Agent.SendOptions): Promise<Response>;
71
+ }
72
+
73
+ export namespace AgentPool {
74
+ export interface Options {
75
+ dialer?: Dialer;
76
+
77
+ poolMaxIdlePerHost?: number;
78
+ poolMaxPerHost?: number;
79
+
80
+ poolIdleTimeout?: number | false;
81
+
82
+ connect?: Agent.ConnectOptions;
83
+
84
+ io?: Agent.IOOptions;
85
+ }
86
+ }
87
+
88
+ export interface AgentConnectOptions extends Agent.ConnectOptions {}
89
+
90
+ export interface AgentPoolOptions extends AgentPool.Options {}
91
+
92
+ export interface SendOptions extends Agent.SendOptions {}
93
+
94
+ export interface AgentIOOptions extends Agent.IOOptions {}
95
+
96
+ export type AgentReaderOptions = Agent.ReaderOptions;
97
+
98
+ export type AgentWriterOptions = Agent.WriterOptions;
@@ -1,27 +1,40 @@
1
- import { ITcpConnection, ITlsConnection } from '@fuman/net';
2
- import { NodeTlsConnectOptions } from '@fuman/node';
1
+ import type { ITcpConnection, ITlsConnection } from "@fuman/net";
2
+ import type { NodeTlsConnectOptions } from "@fuman/node";
3
+
4
+ /**
5
+ * Transport abstraction responsible for opening a connection to an HTTP target.
6
+ */
3
7
  export interface Dialer {
4
- dial(target: Dialer.Target, options?: Dialer.Options): Promise<Dialer.ConnectionLike>;
8
+ dial(
9
+ target: Dialer.Target,
10
+ options?: Dialer.Options,
11
+ ): Promise<Dialer.ConnectionLike>;
5
12
  }
6
- export declare namespace Dialer {
7
- type ConnectionLike = ITcpConnection | ITlsConnection;
8
- interface Target {
13
+
14
+ export namespace Dialer {
15
+ export type ConnectionLike = ITcpConnection | ITlsConnection;
16
+
17
+ export interface Target {
9
18
  address: string;
10
19
  port: number;
11
20
  secure: boolean;
21
+
12
22
  /** Server Name Indication (TLS). Defaults to the host when applicable. */
13
23
  sni?: string;
24
+
14
25
  /** Defaults to ["http/1.1"] when omitted by the dialer implementation. */
15
26
  alpnProtocols?: string[];
27
+
16
28
  /** Extra Node.js TLS options (minVersion, servername, etc.). */
17
29
  extraOptions?: NodeTlsConnectOptions["extraOptions"];
18
30
  }
19
- interface Options {
31
+
32
+ export interface Options {
20
33
  signal?: AbortSignal;
21
34
  }
22
35
  }
36
+
23
37
  /** Back-compat */
24
38
  export type ConnectionLike = Dialer.ConnectionLike;
25
39
  /** Back-compat */
26
- export interface DialTarget extends Dialer.Target {
27
- }
40
+ export interface DialTarget extends Dialer.Target {}
@@ -0,0 +1,2 @@
1
+ export * from "./agent";
2
+ export * from "./dialer";
@@ -0,0 +1,111 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { createAgentPool } from "../src/agent-pool";
3
+ import { RequestAbortedError } from "../src/errors";
4
+ import { createTestServer } from "./test-utils";
5
+
6
+ describe("agent-pool.ts", () => {
7
+ const testServer = createTestServer();
8
+
9
+ afterAll(async () => {
10
+ await testServer.stop();
11
+ });
12
+
13
+ test("pool handles concurrent requests successfully", async () => {
14
+ const pool = createAgentPool(testServer.baseUrl, {
15
+ poolMaxPerHost: 4,
16
+ });
17
+
18
+ try {
19
+ const requests = Array.from({ length: 8 }, (_, index) =>
20
+ pool
21
+ .send({
22
+ url: `${testServer.baseUrl}/echo`,
23
+ method: "POST",
24
+ headers: new Headers({
25
+ "content-type": "application/json",
26
+ }),
27
+ body: JSON.stringify({ index }),
28
+ })
29
+ .then((response) => response.json()),
30
+ );
31
+
32
+ const results = await Promise.all(requests);
33
+
34
+ expect(results).toHaveLength(8);
35
+ for (const result of results) {
36
+ expect(result.method).toBe("POST");
37
+ }
38
+ } finally {
39
+ await pool.close();
40
+ }
41
+ });
42
+
43
+ test("pool queues requests when poolMaxPerHost is small", async () => {
44
+ const pool = createAgentPool(testServer.baseUrl, {
45
+ poolMaxPerHost: 2,
46
+ });
47
+
48
+ try {
49
+ const startedAt = performance.now();
50
+
51
+ const requests = Array.from({ length: 4 }, () =>
52
+ pool
53
+ .send({
54
+ url: `${testServer.baseUrl}/slow`,
55
+ method: "GET",
56
+ })
57
+ .then((response) => response.text()),
58
+ );
59
+
60
+ const results = await Promise.all(requests);
61
+ const elapsed = performance.now() - startedAt;
62
+
63
+ expect(results).toEqual([
64
+ "Finally!",
65
+ "Finally!",
66
+ "Finally!",
67
+ "Finally!",
68
+ ]);
69
+
70
+ expect(elapsed).toBeGreaterThanOrEqual(300);
71
+ } finally {
72
+ await pool.close();
73
+ }
74
+ });
75
+
76
+ test("pool propagates abort signals as RequestAbortedError", async () => {
77
+ const pool = createAgentPool(testServer.baseUrl, {
78
+ poolMaxPerHost: 2,
79
+ });
80
+
81
+ try {
82
+ const controller = new AbortController();
83
+
84
+ const request = pool.send({
85
+ url: `${testServer.baseUrl}/slow`,
86
+ method: "GET",
87
+ signal: controller.signal,
88
+ });
89
+
90
+ setTimeout(() => controller.abort(new Error("abort test")), 50);
91
+
92
+ await expect(request).rejects.toBeInstanceOf(RequestAbortedError);
93
+ } finally {
94
+ await pool.close();
95
+ }
96
+ });
97
+
98
+ test("pool closes cleanly via close()", async () => {
99
+ const pool = createAgentPool(testServer.baseUrl, {
100
+ poolMaxPerHost: 2,
101
+ });
102
+
103
+ const response = await pool.send({
104
+ url: `${testServer.baseUrl}/text`,
105
+ method: "GET",
106
+ });
107
+ expect(await response.text()).toBe("Hello, World!");
108
+
109
+ await expect(pool.close()).resolves.toBeUndefined();
110
+ });
111
+ });