@npy/fetch 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/_internal/decode-stream-error.cjs +18 -0
  2. package/_internal/decode-stream-error.d.cts +11 -0
  3. package/_internal/decode-stream-error.d.ts +11 -0
  4. package/_internal/decode-stream-error.js +18 -0
  5. package/_internal/error-mapping.cjs +44 -0
  6. package/_internal/error-mapping.d.cts +15 -0
  7. package/_internal/error-mapping.d.ts +15 -0
  8. package/_internal/error-mapping.js +41 -0
  9. package/_internal/guards.cjs +5 -6
  10. package/{src/_internal → _internal}/guards.d.cts +2 -0
  11. package/{src/_internal → _internal}/guards.d.ts +2 -0
  12. package/_internal/guards.js +5 -7
  13. package/{src/_internal → _internal}/net.d.cts +1 -2
  14. package/{src/_internal → _internal}/net.d.ts +1 -2
  15. package/_internal/symbols.cjs +4 -0
  16. package/_internal/symbols.d.cts +1 -0
  17. package/_internal/symbols.d.ts +1 -0
  18. package/_internal/symbols.js +4 -0
  19. package/agent-pool.cjs +23 -5
  20. package/agent-pool.d.cts +2 -0
  21. package/agent-pool.d.ts +2 -0
  22. package/agent-pool.js +23 -5
  23. package/agent.cjs +17 -14
  24. package/agent.js +17 -14
  25. package/body.cjs +10 -59
  26. package/body.d.cts +12 -0
  27. package/body.d.ts +12 -0
  28. package/body.js +11 -60
  29. package/dialers/proxy.cjs +7 -0
  30. package/{src/dialers → dialers}/proxy.d.cts +11 -3
  31. package/{src/dialers → dialers}/proxy.d.ts +11 -3
  32. package/dialers/proxy.js +7 -0
  33. package/dialers/tcp.cjs +22 -0
  34. package/{src/dialers → dialers}/tcp.d.cts +23 -2
  35. package/{src/dialers → dialers}/tcp.d.ts +23 -2
  36. package/dialers/tcp.js +22 -0
  37. package/encoding.cjs +32 -13
  38. package/encoding.d.cts +35 -0
  39. package/encoding.d.ts +35 -0
  40. package/encoding.js +32 -13
  41. package/fetch.cjs +279 -43
  42. package/fetch.d.cts +58 -0
  43. package/fetch.d.ts +58 -0
  44. package/fetch.js +278 -43
  45. package/http-client.cjs +47 -5
  46. package/http-client.d.cts +39 -0
  47. package/http-client.d.ts +39 -0
  48. package/http-client.js +47 -5
  49. package/index.cjs +7 -3
  50. package/index.d.cts +14 -1
  51. package/index.d.ts +14 -1
  52. package/index.js +6 -4
  53. package/io/io.cjs +68 -4
  54. package/{src/io → io}/io.d.cts +1 -1
  55. package/{src/io → io}/io.d.ts +1 -1
  56. package/io/io.js +68 -4
  57. package/io/readers.cjs +14 -54
  58. package/io/readers.d.cts +69 -0
  59. package/io/readers.d.ts +69 -0
  60. package/io/readers.js +14 -54
  61. package/io/writers.cjs +10 -5
  62. package/{src/io → io}/writers.d.cts +1 -1
  63. package/{src/io → io}/writers.d.ts +1 -1
  64. package/io/writers.js +11 -6
  65. package/package.json +18 -2
  66. package/types/agent.d.cts +72 -0
  67. package/types/agent.d.ts +72 -0
  68. package/{src/types → types}/dialer.d.cts +3 -0
  69. package/{src/types → types}/dialer.d.ts +3 -0
  70. package/_internal/error-adapters.cjs +0 -146
  71. package/_internal/error-adapters.js +0 -142
  72. package/src/_internal/error-adapters.d.cts +0 -22
  73. package/src/_internal/error-adapters.d.ts +0 -22
  74. package/src/agent-pool.d.cts +0 -2
  75. package/src/agent-pool.d.ts +0 -2
  76. package/src/body.d.cts +0 -23
  77. package/src/body.d.ts +0 -23
  78. package/src/encoding.d.cts +0 -24
  79. package/src/encoding.d.ts +0 -24
  80. package/src/fetch.d.cts +0 -36
  81. package/src/fetch.d.ts +0 -36
  82. package/src/http-client.d.cts +0 -23
  83. package/src/http-client.d.ts +0 -23
  84. package/src/index.d.cts +0 -7
  85. package/src/index.d.ts +0 -7
  86. package/src/io/readers.d.cts +0 -199
  87. package/src/io/readers.d.ts +0 -199
  88. package/src/types/agent.d.cts +0 -128
  89. package/src/types/agent.d.ts +0 -128
  90. package/tests/test-utils.d.cts +0 -8
  91. package/tests/test-utils.d.ts +0 -8
  92. /package/{src/_internal → _internal}/consts.d.cts +0 -0
  93. /package/{src/_internal → _internal}/consts.d.ts +0 -0
  94. /package/{src/_internal → _internal}/promises.d.cts +0 -0
  95. /package/{src/_internal → _internal}/promises.d.ts +0 -0
  96. /package/{src/_internal → _internal}/streams.d.cts +0 -0
  97. /package/{src/_internal → _internal}/streams.d.ts +0 -0
  98. /package/{src/agent.d.cts → agent.d.cts} +0 -0
  99. /package/{src/agent.d.ts → agent.d.ts} +0 -0
  100. /package/{src/dialers → dialers}/index.d.cts +0 -0
  101. /package/{src/dialers → dialers}/index.d.ts +0 -0
  102. /package/{src/errors.d.cts → errors.d.cts} +0 -0
  103. /package/{src/errors.d.ts → errors.d.ts} +0 -0
  104. /package/{src/io → io}/_utils.d.cts +0 -0
  105. /package/{src/io → io}/_utils.d.ts +0 -0
  106. /package/{src/io → io}/buf-writer.d.cts +0 -0
  107. /package/{src/io → io}/buf-writer.d.ts +0 -0
  108. /package/{src/types → types}/index.d.cts +0 -0
  109. /package/{src/types → types}/index.d.ts +0 -0
package/fetch.js CHANGED
@@ -1,53 +1,81 @@
1
+ import { toWebBodyReadError, toWebFetchError } from "./_internal/error-mapping.js";
2
+ import { bodyErrorMapperSymbol } from "./_internal/symbols.js";
3
+ import { isBlob, isFormData, isIterable, isMultipartFormDataStream, isReadable, isReadableStream, isURLSearchParameters } from "./_internal/guards.js";
4
+ import { fromRequestBody } from "./body.js";
5
+ import { ProxyDialer } from "./dialers/proxy.js";
1
6
  import { AutoDialer } from "./dialers/tcp.js";
2
- import { toWebBodyReadError, toWebFetchError, wrapResponseBodyErrors } from "./_internal/error-adapters.js";
3
7
  import { HttpClient } from "./http-client.js";
8
+ import { parse, stringify } from "@npy/proxy-kit";
4
9
  //#region src/fetch.ts
10
+ var MAX_REDIRECTS = 20;
11
+ var REDIRECT_STATUSES = new Set([
12
+ 301,
13
+ 302,
14
+ 303,
15
+ 307,
16
+ 308
17
+ ]);
18
+ var SENSITIVE_REDIRECT_HEADERS = [
19
+ "authorization",
20
+ "cookie",
21
+ "proxy-authorization"
22
+ ];
23
+ var BODY_HEADERS = [
24
+ "content-encoding",
25
+ "content-language",
26
+ "content-length",
27
+ "content-location",
28
+ "content-type",
29
+ "transfer-encoding"
30
+ ];
5
31
  function createDefaultHttpClient(options = {}) {
6
32
  return new HttpClient({
7
33
  ...options,
8
34
  dialer: options.dialer ?? new AutoDialer()
9
35
  });
10
36
  }
37
+ /**
38
+ * Normalizes any supported header input into a {@link Headers} instance.
39
+ *
40
+ * @remarks
41
+ * If the input is already a {@link Headers} object, the same instance is returned.
42
+ * Tuple arrays and plain records are copied into a new {@link Headers}.
43
+ */
11
44
  function normalizeHeaders(headers) {
12
45
  if (headers instanceof Headers) return headers;
13
- const normalized = new Headers();
46
+ const result = new Headers();
47
+ if (!headers) return result;
14
48
  if (Array.isArray(headers)) {
15
- headers.forEach(([key, value]) => {
16
- normalized.append(key, value);
17
- });
18
- return normalized;
49
+ for (const [key, value] of headers) result.append(key, value);
50
+ return result;
19
51
  }
20
- if (headers) Object.entries(headers).forEach(([key, value]) => {
21
- if (Array.isArray(value)) value.forEach((entry) => {
22
- normalized.append(key, entry);
23
- });
24
- else if (value !== void 0) normalized.append(key, value);
25
- });
26
- return normalized;
52
+ for (const [key, value] of Object.entries(headers)) if (Array.isArray(value)) for (const entry of value) result.append(key, entry);
53
+ else if (value !== void 0) result.append(key, value);
54
+ return result;
27
55
  }
28
56
  function resolveUrl(input) {
29
57
  if (input instanceof URL) return input;
30
- if (input instanceof Request) return new URL(input.url);
31
- return new URL(String(input));
58
+ return new URL(input instanceof Request ? input.url : String(input));
32
59
  }
33
60
  function resolveMethod(input, init) {
34
- if (init.method != null) return init.method.toUpperCase();
35
- if (input instanceof Request) return input.method.toUpperCase();
36
- return "GET";
61
+ return (init.method ?? (input instanceof Request ? input.method : void 0))?.toUpperCase().trim() ?? "GET";
37
62
  }
38
63
  function resolveHeaders(input, init) {
39
- if (init.headers !== void 0) return normalizeHeaders(init.headers);
40
- if (input instanceof Request) return normalizeHeaders(input.headers);
41
- return new Headers();
64
+ return normalizeHeaders(init.headers !== void 0 ? init.headers : input instanceof Request ? input.headers : void 0);
42
65
  }
43
66
  function resolveSignal(input, init) {
44
67
  return init.signal ?? (input instanceof Request ? input.signal : void 0);
45
68
  }
46
69
  function resolveBody(input, init) {
47
70
  if (init.body !== void 0) return init.body;
48
- if (!(input instanceof Request)) return;
49
- if (input.bodyUsed) throw new TypeError("Request body has already been used");
50
- return input.body;
71
+ return input instanceof Request ? fromRequestBody(input) : void 0;
72
+ }
73
+ function resolveRedirectMode(input, init) {
74
+ return init.redirect ?? (input instanceof Request ? input.redirect : "follow");
75
+ }
76
+ function getBodyReplaySource(input, init, method, body, redirect) {
77
+ if (redirect !== "follow" || init.body !== void 0 || body == null || method === "GET" || method === "HEAD" || !(input instanceof Request)) return;
78
+ return input.clone();
51
79
  }
52
80
  function assertValidFetchUrl(url) {
53
81
  if (url.username || url.password) throw new TypeError("Request URL must not include embedded credentials");
@@ -57,36 +85,236 @@ function assertValidFetchBody(method, body) {
57
85
  if (body == null) return;
58
86
  if (method === "GET" || method === "HEAD") throw new TypeError(`Request with ${method} method cannot have a body`);
59
87
  }
60
- async function fetchImpl(input, init) {
88
+ function lookupEnvProxy(...names) {
89
+ for (const name of names) {
90
+ const normalized = (process.env[name] ?? process.env[name.toLowerCase()])?.trim();
91
+ if (normalized) return normalized;
92
+ }
93
+ }
94
+ function resolveProxyFromEnv(url) {
95
+ const socksProxy = lookupEnvProxy("SOCKS5_PROXY", "SOCKS_PROXY");
96
+ if (socksProxy) return socksProxy;
97
+ if (url.protocol === "https:") return lookupEnvProxy("HTTPS_PROXY", "HTTP_PROXY");
98
+ return lookupEnvProxy("HTTP_PROXY");
99
+ }
100
+ function normalizeProxyConfig(proxy) {
101
+ const parsed = typeof proxy === "string" ? parse(proxy, { strict: true }) : proxy;
102
+ if (parsed == null) throw new TypeError(`Invalid proxy string: ${String(proxy)}`);
103
+ const key = stringify(parsed, {
104
+ strict: true,
105
+ format: "user:pass@ip:port"
106
+ });
107
+ if (!key) throw new TypeError("Failed to normalize proxy configuration");
108
+ return {
109
+ key,
110
+ proxy: parsed
111
+ };
112
+ }
113
+ function resolveNormalizedProxy(url, proxy) {
114
+ if (proxy === null) return null;
115
+ if (proxy !== void 0) return normalizeProxyConfig(proxy);
116
+ const envProxy = resolveProxyFromEnv(url);
117
+ return envProxy ? normalizeProxyConfig(envProxy) : null;
118
+ }
119
+ function annotateResponse(response, url, redirected) {
120
+ try {
121
+ if (redirected) Object.defineProperties(response, {
122
+ redirected: {
123
+ configurable: true,
124
+ enumerable: true,
125
+ value: true,
126
+ writable: false
127
+ },
128
+ url: {
129
+ configurable: true,
130
+ enumerable: true,
131
+ value: url,
132
+ writable: false
133
+ }
134
+ });
135
+ else Object.defineProperty(response, "url", {
136
+ configurable: true,
137
+ enumerable: true,
138
+ value: url,
139
+ writable: false
140
+ });
141
+ } catch {}
142
+ return response;
143
+ }
144
+ async function discardResponse(response) {
145
+ try {
146
+ await response.body?.cancel();
147
+ } catch {}
148
+ }
149
+ function isRedirectResponse(response) {
150
+ return REDIRECT_STATUSES.has(response.status) && response.headers.get("location") != null;
151
+ }
152
+ function isReplayableBody(body) {
153
+ if (body == null || typeof body === "string" || body instanceof Uint8Array || isBlob(body) || isFormData(body) || isURLSearchParameters(body)) return true;
154
+ if (isReadableStream(body) || isReadable(body) || isMultipartFormDataStream(body) || isIterable(body)) return false;
155
+ return true;
156
+ }
157
+ async function resolveReplayableBody(current) {
158
+ if (isReplayableBody(current.body)) return current.body;
159
+ if (current.replayBodyRequest) {
160
+ const buffer = await current.replayBodyRequest.arrayBuffer();
161
+ return buffer.byteLength > 0 ? new Uint8Array(buffer) : null;
162
+ }
163
+ throw new TypeError("Cannot follow redirect with a non-replayable request body");
164
+ }
165
+ function shouldDropBodyOnRedirect(status, method) {
166
+ if (status === 303) return method !== "HEAD";
167
+ return (status === 301 || status === 302) && method === "POST";
168
+ }
169
+ function resolveRedirectMethod(status, method) {
170
+ if (status === 303 && method !== "HEAD" || (status === 301 || status === 302) && method === "POST") return "GET";
171
+ return method;
172
+ }
173
+ function createRedirectHeaders(current, nextUrl, dropBody) {
174
+ const headers = new Headers(current.headers);
175
+ const isCrossOrigin = current.url.origin !== nextUrl.origin;
176
+ const isDowngrade = current.url.protocol === "https:" && nextUrl.protocol === "http:";
177
+ headers.delete("host");
178
+ if (isCrossOrigin) for (const name of SENSITIVE_REDIRECT_HEADERS) headers.delete(name);
179
+ if (dropBody) for (const name of BODY_HEADERS) headers.delete(name);
180
+ if (!headers.has("referer") && !isDowngrade) headers.set("referer", current.url.toString());
181
+ return headers;
182
+ }
183
+ async function buildRedirectRequest(current, response) {
184
+ const location = response.headers.get("location");
185
+ if (!location) return current;
186
+ const nextUrl = new URL(location, current.url);
187
+ const nextMethod = resolveRedirectMethod(response.status, current.method);
188
+ const dropBody = shouldDropBodyOnRedirect(response.status, current.method);
189
+ const nextHeaders = createRedirectHeaders(current, nextUrl, dropBody);
190
+ const nextBody = dropBody ? void 0 : await resolveReplayableBody(current);
191
+ return {
192
+ ...current,
193
+ url: nextUrl,
194
+ method: nextMethod,
195
+ headers: nextHeaders,
196
+ body: nextBody,
197
+ replayBodyRequest: void 0
198
+ };
199
+ }
200
+ async function sendPreparedRequest(prepared, getProxyClient) {
201
+ assertValidFetchUrl(prepared.url);
202
+ assertValidFetchBody(prepared.method, prepared.body);
203
+ const normalizedProxy = resolveNormalizedProxy(prepared.url, prepared.proxy);
204
+ const client = normalizedProxy ? getProxyClient(prepared.baseClient, normalizedProxy) : prepared.baseClient;
205
+ try {
206
+ const sendOptions = Object.defineProperty({
207
+ url: prepared.url,
208
+ method: prepared.method,
209
+ headers: prepared.headers,
210
+ body: prepared.body ?? null,
211
+ signal: prepared.signal
212
+ }, bodyErrorMapperSymbol, {
213
+ configurable: false,
214
+ enumerable: false,
215
+ writable: false,
216
+ value: (error) => toWebBodyReadError(error, prepared.signal)
217
+ });
218
+ return await client.send(sendOptions);
219
+ } catch (error) {
220
+ throw toWebFetchError(error, prepared.signal);
221
+ }
222
+ }
223
+ function prepareRequest(input, init, defaultHttpClient) {
61
224
  const url = resolveUrl(input);
62
- assertValidFetchUrl(url);
63
225
  const method = resolveMethod(input, init);
64
226
  const headers = resolveHeaders(input, init);
65
227
  const body = resolveBody(input, init);
66
228
  const signal = resolveSignal(input, init);
67
- assertValidFetchBody(method, body);
68
- try {
69
- return wrapResponseBodyErrors(await init.client.send({
70
- url,
71
- method,
72
- headers,
73
- body: body ?? null,
74
- signal
75
- }), (error) => toWebBodyReadError(error, signal));
76
- } catch (error) {
77
- throw toWebFetchError(error, signal);
78
- }
229
+ const redirect = resolveRedirectMode(input, init);
230
+ return {
231
+ url,
232
+ method,
233
+ headers,
234
+ body,
235
+ signal,
236
+ redirect,
237
+ baseClient: init.client ?? defaultHttpClient,
238
+ proxy: init.proxy,
239
+ replayBodyRequest: getBodyReplaySource(input, init, method, body, redirect)
240
+ };
79
241
  }
242
+ /**
243
+ * Creates a fetch-compatible client backed by {@link HttpClient}.
244
+ *
245
+ * @remarks
246
+ * The returned function follows the standard fetch shape, but uses this library's
247
+ * connection pooling, proxy support and body/error mapping rules.
248
+ *
249
+ * When no client is provided, an internal {@link HttpClient} is created and owned
250
+ * by the returned fetch-like function. Calling {@link FetchLike.close} closes that
251
+ * internal client and any proxy-specific clients created on demand.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * const fetchLike = createFetch();
256
+ * const response = await fetchLike("https://httpbin.org/anything");
257
+ * const data = await response.json();
258
+ * await fetchLike.close();
259
+ * ```
260
+ */
80
261
  function createFetch(client) {
81
262
  const defaultHttpClient = client ?? createDefaultHttpClient();
263
+ const proxyClientsByBase = /* @__PURE__ */ new WeakMap();
264
+ const ownedProxyClients = /* @__PURE__ */ new Set();
265
+ let closePromise;
266
+ const getProxyClient = (baseClient, proxy) => {
267
+ let clients = proxyClientsByBase.get(baseClient);
268
+ if (!clients) {
269
+ clients = /* @__PURE__ */ new Map();
270
+ proxyClientsByBase.set(baseClient, clients);
271
+ }
272
+ const existing = clients.get(proxy.key);
273
+ if (existing) return existing;
274
+ const proxyClient = new HttpClient({
275
+ ...baseClient.options,
276
+ dialer: new ProxyDialer(proxy.proxy)
277
+ });
278
+ clients.set(proxy.key, proxyClient);
279
+ ownedProxyClients.add(proxyClient);
280
+ return proxyClient;
281
+ };
82
282
  const fetchLike = (async (input, init = {}) => {
83
- return fetchImpl(input, init.client == null ? {
84
- ...init,
85
- client: defaultHttpClient
86
- } : init);
283
+ let prepared = prepareRequest(input, init, defaultHttpClient);
284
+ let redirected = false;
285
+ let redirects = 0;
286
+ for (;;) {
287
+ const response = await sendPreparedRequest(prepared, getProxyClient);
288
+ if (!isRedirectResponse(response) || prepared.redirect === "manual") return annotateResponse(response, prepared.url.toString(), redirected);
289
+ if (prepared.redirect === "error") {
290
+ await discardResponse(response);
291
+ throw new TypeError(`fetch failed: redirect mode is set to "error"`);
292
+ }
293
+ if (redirects >= MAX_REDIRECTS) {
294
+ await discardResponse(response);
295
+ throw new TypeError(`fetch failed: maximum redirect count exceeded`);
296
+ }
297
+ prepared = await buildRedirectRequest(prepared, response);
298
+ await discardResponse(response);
299
+ redirected = true;
300
+ redirects += 1;
301
+ }
87
302
  });
88
303
  const close = async () => {
89
- if (client == null) await defaultHttpClient.close();
304
+ if (closePromise) return closePromise;
305
+ const promise = (async () => {
306
+ const proxyClients = Array.from(ownedProxyClients);
307
+ ownedProxyClients.clear();
308
+ const errors = (await Promise.allSettled([...proxyClients.map((c) => c.close()), ...client == null ? [defaultHttpClient.close()] : []])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
309
+ if (errors.length === 1) throw errors[0];
310
+ if (errors.length > 1) throw new AggregateError(errors, "Failed to close one or more fetch clients");
311
+ })();
312
+ closePromise = promise;
313
+ try {
314
+ await promise;
315
+ } finally {
316
+ if (closePromise === promise) closePromise = void 0;
317
+ }
90
318
  };
91
319
  Object.defineProperties(fetchLike, {
92
320
  client: {
@@ -110,6 +338,13 @@ function createFetch(client) {
110
338
  });
111
339
  return fetchLike;
112
340
  }
341
+ /**
342
+ * Default fetch-compatible client created with {@link createFetch}.
343
+ *
344
+ * @remarks
345
+ * This singleton owns its internal {@link HttpClient}. Call {@link FetchLike.close}
346
+ * when you want to release pooled connections explicitly.
347
+ */
113
348
  var fetch = createFetch();
114
349
  //#endregion
115
350
  export { createFetch, fetch as default, normalizeHeaders };
package/http-client.cjs CHANGED
@@ -1,20 +1,62 @@
1
1
  const require_agent_pool = require("./agent-pool.cjs");
2
2
  //#region src/http-client.ts
3
+ /**
4
+ * Advanced HTTP client with per-origin pooling and explicit lifecycle control.
5
+ *
6
+ * @remarks
7
+ * Use this API when you want direct access to the library's richer error model and
8
+ * transport options instead of the fetch-like compatibility layer.
9
+ *
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const client = new HttpClient();
14
+ * const response = await client.send({
15
+ * url: "https://httpbin.org/anything",
16
+ * method: "GET",
17
+ * });
18
+ * await client.close();
19
+ * ```
20
+ */
3
21
  var HttpClient = class {
4
22
  #agentPools = /* @__PURE__ */ new Map();
5
23
  #agentPoolOptions;
24
+ #closePromise;
6
25
  constructor(options = {}) {
7
26
  this.#agentPoolOptions = { ...options };
8
27
  }
28
+ get options() {
29
+ return this.#agentPoolOptions;
30
+ }
9
31
  async send(options) {
10
32
  return this.#getOrCreateAgentPool(options.url).send(options);
11
33
  }
34
+ /**
35
+ * Closes all pooled connections owned by this client.
36
+ *
37
+ * @remarks
38
+ * After closing, future requests may recreate pools as needed.
39
+ */
12
40
  async close() {
13
- const entries = Array.from(this.#agentPools.entries());
14
- const failed = (await Promise.allSettled(entries.map(([origin, agentPool]) => agentPool.close().then(() => {
15
- this.#agentPools.delete(origin);
16
- })))).find((r) => r.status === "rejected");
17
- if (failed) throw failed.reason;
41
+ if (this.#closePromise) return this.#closePromise;
42
+ const promise = (async () => {
43
+ const entries = Array.from(this.#agentPools.entries());
44
+ const errors = (await Promise.allSettled(entries.map(async ([origin, agentPool]) => {
45
+ try {
46
+ await agentPool.close();
47
+ } finally {
48
+ this.#agentPools.delete(origin);
49
+ }
50
+ }))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
51
+ if (errors.length === 1) throw errors[0];
52
+ if (errors.length > 1) throw new AggregateError(errors, "Failed to close one or more agent pools");
53
+ })();
54
+ this.#closePromise = promise;
55
+ try {
56
+ await promise;
57
+ } finally {
58
+ if (this.#closePromise === promise) this.#closePromise = void 0;
59
+ }
18
60
  }
19
61
  async [Symbol.asyncDispose]() {
20
62
  await this.close();
@@ -0,0 +1,39 @@
1
+ import { Agent, AgentPool } from './types/agent';
2
+ /**
3
+ * Advanced HTTP client with per-origin pooling and explicit lifecycle control.
4
+ *
5
+ * @remarks
6
+ * Use this API when you want direct access to the library's richer error model and
7
+ * transport options instead of the fetch-like compatibility layer.
8
+ *
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const client = new HttpClient();
13
+ * const response = await client.send({
14
+ * url: "https://httpbin.org/anything",
15
+ * method: "GET",
16
+ * });
17
+ * await client.close();
18
+ * ```
19
+ */
20
+ export declare class HttpClient implements AsyncDisposable {
21
+ #private;
22
+ constructor(options?: HttpClient.Options);
23
+ get options(): Readonly<HttpClient.Options>;
24
+ send(options: Agent.SendOptions): Promise<Response>;
25
+ /**
26
+ * Closes all pooled connections owned by this client.
27
+ *
28
+ * @remarks
29
+ * After closing, future requests may recreate pools as needed.
30
+ */
31
+ close(): Promise<void>;
32
+ [Symbol.asyncDispose](): Promise<void>;
33
+ }
34
+ export declare namespace HttpClient {
35
+ interface Options extends AgentPool.Options {
36
+ }
37
+ }
38
+ export interface HttpClientOptions extends HttpClient.Options {
39
+ }
@@ -0,0 +1,39 @@
1
+ import { Agent, AgentPool } from './types/agent';
2
+ /**
3
+ * Advanced HTTP client with per-origin pooling and explicit lifecycle control.
4
+ *
5
+ * @remarks
6
+ * Use this API when you want direct access to the library's richer error model and
7
+ * transport options instead of the fetch-like compatibility layer.
8
+ *
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const client = new HttpClient();
13
+ * const response = await client.send({
14
+ * url: "https://httpbin.org/anything",
15
+ * method: "GET",
16
+ * });
17
+ * await client.close();
18
+ * ```
19
+ */
20
+ export declare class HttpClient implements AsyncDisposable {
21
+ #private;
22
+ constructor(options?: HttpClient.Options);
23
+ get options(): Readonly<HttpClient.Options>;
24
+ send(options: Agent.SendOptions): Promise<Response>;
25
+ /**
26
+ * Closes all pooled connections owned by this client.
27
+ *
28
+ * @remarks
29
+ * After closing, future requests may recreate pools as needed.
30
+ */
31
+ close(): Promise<void>;
32
+ [Symbol.asyncDispose](): Promise<void>;
33
+ }
34
+ export declare namespace HttpClient {
35
+ interface Options extends AgentPool.Options {
36
+ }
37
+ }
38
+ export interface HttpClientOptions extends HttpClient.Options {
39
+ }
package/http-client.js CHANGED
@@ -1,20 +1,62 @@
1
1
  import { createAgentPool } from "./agent-pool.js";
2
2
  //#region src/http-client.ts
3
+ /**
4
+ * Advanced HTTP client with per-origin pooling and explicit lifecycle control.
5
+ *
6
+ * @remarks
7
+ * Use this API when you want direct access to the library's richer error model and
8
+ * transport options instead of the fetch-like compatibility layer.
9
+ *
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const client = new HttpClient();
14
+ * const response = await client.send({
15
+ * url: "https://httpbin.org/anything",
16
+ * method: "GET",
17
+ * });
18
+ * await client.close();
19
+ * ```
20
+ */
3
21
  var HttpClient = class {
4
22
  #agentPools = /* @__PURE__ */ new Map();
5
23
  #agentPoolOptions;
24
+ #closePromise;
6
25
  constructor(options = {}) {
7
26
  this.#agentPoolOptions = { ...options };
8
27
  }
28
+ get options() {
29
+ return this.#agentPoolOptions;
30
+ }
9
31
  async send(options) {
10
32
  return this.#getOrCreateAgentPool(options.url).send(options);
11
33
  }
34
+ /**
35
+ * Closes all pooled connections owned by this client.
36
+ *
37
+ * @remarks
38
+ * After closing, future requests may recreate pools as needed.
39
+ */
12
40
  async close() {
13
- const entries = Array.from(this.#agentPools.entries());
14
- const failed = (await Promise.allSettled(entries.map(([origin, agentPool]) => agentPool.close().then(() => {
15
- this.#agentPools.delete(origin);
16
- })))).find((r) => r.status === "rejected");
17
- if (failed) throw failed.reason;
41
+ if (this.#closePromise) return this.#closePromise;
42
+ const promise = (async () => {
43
+ const entries = Array.from(this.#agentPools.entries());
44
+ const errors = (await Promise.allSettled(entries.map(async ([origin, agentPool]) => {
45
+ try {
46
+ await agentPool.close();
47
+ } finally {
48
+ this.#agentPools.delete(origin);
49
+ }
50
+ }))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
51
+ if (errors.length === 1) throw errors[0];
52
+ if (errors.length > 1) throw new AggregateError(errors, "Failed to close one or more agent pools");
53
+ })();
54
+ this.#closePromise = promise;
55
+ try {
56
+ await promise;
57
+ } finally {
58
+ if (this.#closePromise === promise) this.#closePromise = void 0;
59
+ }
18
60
  }
19
61
  async [Symbol.asyncDispose]() {
20
62
  await this.close();
package/index.cjs CHANGED
@@ -1,16 +1,17 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_net = require("./_internal/net.cjs");
3
+ const require_errors = require("./errors.cjs");
4
+ const require_encoding = require("./encoding.cjs");
3
5
  const require_body = require("./body.cjs");
6
+ const require_agent = require("./agent.cjs");
4
7
  const require_proxy = require("./dialers/proxy.cjs");
5
8
  const require_tcp = require("./dialers/tcp.cjs");
6
- const require_encoding = require("./encoding.cjs");
7
- const require_errors = require("./errors.cjs");
9
+ const require_agent_pool = require("./agent-pool.cjs");
8
10
  const require_http_client = require("./http-client.cjs");
9
11
  const require_fetch = require("./fetch.cjs");
10
12
  exports.AgentBusyError = require_errors.AgentBusyError;
11
13
  exports.AgentClosedError = require_errors.AgentClosedError;
12
14
  exports.AutoDialer = require_tcp.AutoDialer;
13
- exports.Body = require_body.Body;
14
15
  exports.ConnectTimeoutError = require_errors.ConnectTimeoutError;
15
16
  exports.ConnectionError = require_errors.ConnectionError;
16
17
  exports.ErrorType = require_errors.ErrorType;
@@ -32,6 +33,8 @@ exports.UnsupportedMethodError = require_errors.UnsupportedMethodError;
32
33
  exports.UnsupportedProtocolError = require_errors.UnsupportedProtocolError;
33
34
  exports.connectTcp = require_net.connectTcp;
34
35
  exports.connectTls = require_net.connectTls;
36
+ exports.createAgent = require_agent.createAgent;
37
+ exports.createAgentPool = require_agent_pool.createAgentPool;
35
38
  exports.createDecoders = require_encoding.createDecoders;
36
39
  exports.createEncoders = require_encoding.createEncoders;
37
40
  exports.createFetch = require_fetch.createFetch;
@@ -39,6 +42,7 @@ exports.decodeStream = require_encoding.decodeStream;
39
42
  exports.encodeStream = require_encoding.encodeStream;
40
43
  exports.extractBody = require_body.extractBody;
41
44
  exports.fetch = require_fetch;
45
+ exports.fromRequestBody = require_body.fromRequestBody;
42
46
  exports.getFormDataLength = require_body.getFormDataLength;
43
47
  exports.normalizeHeaders = require_fetch.normalizeHeaders;
44
48
  exports.resolveHostPort = require_tcp.resolveHostPort;
package/index.d.cts CHANGED
@@ -1 +1,14 @@
1
- export * from "./src/index.js"
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';
package/index.d.ts CHANGED
@@ -1 +1,14 @@
1
- export * from "./src/index.js"
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';
package/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { connectTcp, connectTls, upgradeTls } from "./_internal/net.js";
2
- import { Body, extractBody, getFormDataLength } from "./body.js";
2
+ import { AgentBusyError, AgentClosedError, ConnectTimeoutError, ConnectionError, ErrorType, FetchError, FetchErrorCode, HttpStatusError, OriginMismatchError, RequestAbortedError, RequestWriteError, ResponseBodyError, ResponseDecodeError, ResponseHeaderError, UnsupportedAlpnProtocolError, UnsupportedMethodError, UnsupportedProtocolError } from "./errors.js";
3
+ import { createDecoders, createEncoders, decodeStream, encodeStream } from "./encoding.js";
4
+ import { extractBody, fromRequestBody, getFormDataLength } from "./body.js";
5
+ import { createAgent } from "./agent.js";
3
6
  import { ProxyDialer } from "./dialers/proxy.js";
4
7
  import { AutoDialer, TcpDialer, TlsDialer, resolveHostPort } from "./dialers/tcp.js";
5
- import { createDecoders, createEncoders, decodeStream, encodeStream } from "./encoding.js";
6
- import { AgentBusyError, AgentClosedError, ConnectTimeoutError, ConnectionError, ErrorType, FetchError, FetchErrorCode, HttpStatusError, OriginMismatchError, RequestAbortedError, RequestWriteError, ResponseBodyError, ResponseDecodeError, ResponseHeaderError, UnsupportedAlpnProtocolError, UnsupportedMethodError, UnsupportedProtocolError } from "./errors.js";
8
+ import { createAgentPool } from "./agent-pool.js";
7
9
  import { HttpClient } from "./http-client.js";
8
10
  import fetch, { createFetch, normalizeHeaders } from "./fetch.js";
9
- export { AgentBusyError, AgentClosedError, AutoDialer, Body, ConnectTimeoutError, ConnectionError, ErrorType, FetchError, FetchErrorCode, HttpClient, HttpStatusError, OriginMismatchError, ProxyDialer, RequestAbortedError, RequestWriteError, ResponseBodyError, ResponseDecodeError, ResponseHeaderError, TcpDialer, TlsDialer, UnsupportedAlpnProtocolError, UnsupportedMethodError, UnsupportedProtocolError, connectTcp, connectTls, createDecoders, createEncoders, createFetch, decodeStream, encodeStream, extractBody, fetch, getFormDataLength, normalizeHeaders, resolveHostPort, upgradeTls };
11
+ export { AgentBusyError, AgentClosedError, AutoDialer, ConnectTimeoutError, ConnectionError, ErrorType, FetchError, FetchErrorCode, HttpClient, HttpStatusError, OriginMismatchError, ProxyDialer, RequestAbortedError, RequestWriteError, ResponseBodyError, ResponseDecodeError, ResponseHeaderError, TcpDialer, TlsDialer, UnsupportedAlpnProtocolError, UnsupportedMethodError, UnsupportedProtocolError, connectTcp, connectTls, createAgent, createAgentPool, createDecoders, createEncoders, createFetch, decodeStream, encodeStream, extractBody, fetch, fromRequestBody, getFormDataLength, normalizeHeaders, resolveHostPort, upgradeTls };