@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,162 @@
1
+ import type { NodeTlsConnectOptions } from "@fuman/node";
2
+ import { connectTcp, connectTls } from "../_internal/net";
3
+ import type { Dialer } from "../types/dialer";
4
+
5
+ const DEFAULT_TCP_PORT = 80;
6
+ const DEFAULT_TLS_PORT = 443;
7
+ const DEFAULT_HTTP_ALPN_PROTOCOLS = ["http/1.1"] as const;
8
+
9
+ export type HostPort = {
10
+ address: string;
11
+ port: number;
12
+ };
13
+
14
+ function parsePort(value: string | number): number {
15
+ if (typeof value === "number") {
16
+ if (!Number.isInteger(value) || value <= 0 || value > 65535) {
17
+ throw new TypeError(`Invalid port: ${String(value)}`);
18
+ }
19
+
20
+ return value;
21
+ }
22
+
23
+ if (!/^\d+$/.test(value)) {
24
+ throw new TypeError(`Invalid port: ${JSON.stringify(value)}`);
25
+ }
26
+
27
+ const parsed = Number.parseInt(value, 10);
28
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
29
+ throw new TypeError(`Invalid port: ${JSON.stringify(value)}`);
30
+ }
31
+
32
+ return parsed;
33
+ }
34
+
35
+ /**
36
+ * Resolves the effective network address and port for a URL or dial target.
37
+ *
38
+ * @remarks
39
+ * When the input does not include an explicit port, the provided default port is used.
40
+ */
41
+ export function resolveHostPort(
42
+ target: URL | Dialer.Target,
43
+ defaultPort: number,
44
+ ): HostPort {
45
+ const address = target instanceof URL ? target.hostname : target.address;
46
+
47
+ if (!address) {
48
+ throw new TypeError("Target address is required");
49
+ }
50
+
51
+ const port =
52
+ target instanceof URL
53
+ ? parsePort(target.port || String(defaultPort))
54
+ : parsePort(target.port || defaultPort);
55
+
56
+ return { address, port };
57
+ }
58
+
59
+ /**
60
+ * Dialer for plain TCP targets.
61
+ *
62
+ * @remarks
63
+ * This dialer rejects secure targets and is intended for HTTP over raw TCP.
64
+ */
65
+ export class TcpDialer implements Dialer {
66
+ async dial(
67
+ target: Dialer.Target,
68
+ options: Dialer.Options = {},
69
+ ): Promise<Dialer.ConnectionLike> {
70
+ if (target.secure) {
71
+ throw new Error("TcpDialer cannot dial a secure target");
72
+ }
73
+
74
+ const endpoint = resolveHostPort(target, DEFAULT_TCP_PORT);
75
+
76
+ return connectTcp({
77
+ ...endpoint,
78
+ signal: options.signal,
79
+ });
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Dialer for TLS targets.
85
+ *
86
+ * @remarks
87
+ * This dialer rejects insecure targets and applies TLS-specific options such as
88
+ * CA certificates, SNI and ALPN.
89
+ */
90
+ export class TlsDialer implements Dialer {
91
+ readonly #options: Readonly<TlsDialer.Options>;
92
+
93
+ constructor(options: TlsDialer.Options = {}) {
94
+ this.#options = { ...options };
95
+ }
96
+
97
+ async dial(
98
+ target: Dialer.Target,
99
+ options: Dialer.Options = {},
100
+ ): Promise<Dialer.ConnectionLike> {
101
+ if (!target.secure) {
102
+ throw new Error("TlsDialer cannot dial an insecure target");
103
+ }
104
+
105
+ const endpoint = resolveHostPort(target, DEFAULT_TLS_PORT);
106
+ const extraOptions =
107
+ this.#options.extraOptions || target.extraOptions
108
+ ? {
109
+ ...this.#options.extraOptions,
110
+ ...target.extraOptions,
111
+ }
112
+ : undefined;
113
+
114
+ return connectTls({
115
+ ...endpoint,
116
+ signal: options.signal,
117
+ caCerts: this.#options.caCerts,
118
+ sni: target.sni ?? this.#options.sni ?? endpoint.address,
119
+ alpnProtocols: target.alpnProtocols ??
120
+ this.#options.alpnProtocols ?? [...DEFAULT_HTTP_ALPN_PROTOCOLS],
121
+ extraOptions,
122
+ });
123
+ }
124
+ }
125
+
126
+ export namespace TlsDialer {
127
+ export interface Options {
128
+ caCerts?: string[];
129
+ sni?: string;
130
+ alpnProtocols?: string[];
131
+ extraOptions?: NodeTlsConnectOptions["extraOptions"];
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Selects {@link TcpDialer} or {@link TlsDialer} based on the target security mode.
137
+ */
138
+ export class AutoDialer implements Dialer {
139
+ readonly tcpDialer: TcpDialer;
140
+ readonly tlsDialer: TlsDialer;
141
+
142
+ constructor(options: AutoDialer.Options = {}) {
143
+ this.tcpDialer = options.tcp ?? new TcpDialer();
144
+ this.tlsDialer = options.tls ?? new TlsDialer();
145
+ }
146
+
147
+ dial(
148
+ target: Dialer.Target,
149
+ options: Dialer.Options = {},
150
+ ): Promise<Dialer.ConnectionLike> {
151
+ return target.secure
152
+ ? this.tlsDialer.dial(target, options)
153
+ : this.tcpDialer.dial(target, options);
154
+ }
155
+ }
156
+
157
+ export namespace AutoDialer {
158
+ export interface Options {
159
+ tcp?: TcpDialer;
160
+ tls?: TlsDialer;
161
+ }
162
+ }
@@ -0,0 +1,222 @@
1
+ import { Readable } from "node:stream";
2
+ import { nodeReadableToWeb } from "@fuman/node";
3
+ import { DecodeStreamError } from "./_internal/decode-stream-error";
4
+
5
+ export type ByteStream = ReadableStream<Uint8Array>;
6
+ type ByteSource = ByteStream | AsyncIterable<Uint8Array>;
7
+ export type ByteTransform = TransformStream<Uint8Array, Uint8Array>;
8
+
9
+ function applyTransforms(
10
+ stream: ByteSource,
11
+ contentEncoding: string | string[] | undefined,
12
+ factory: (contentEncoding?: string | string[]) => ByteTransform[],
13
+ ): ByteSource {
14
+ const transforms = factory(contentEncoding);
15
+ if (transforms.length === 0) return stream;
16
+
17
+ let result: ByteStream;
18
+
19
+ if (stream instanceof ReadableStream) {
20
+ result = stream;
21
+ } else {
22
+ result = nodeReadableToWeb(Readable.from(stream));
23
+ }
24
+
25
+ for (const t of transforms) {
26
+ result = result.pipeThrough(t);
27
+ }
28
+
29
+ return result;
30
+ }
31
+
32
+ /**
33
+ * Applies decoding transforms for the given content-encoding list.
34
+ *
35
+ * @remarks
36
+ * If no supported encodings are provided, the original source is returned unchanged.
37
+ * Async iterables are converted to Web Streams only when transforms are required.
38
+ */
39
+ export function decodeStream(
40
+ stream: ByteStream,
41
+ contentEncoding?: string | string[],
42
+ ): ByteStream;
43
+ export function decodeStream(
44
+ stream: AsyncIterable<Uint8Array>,
45
+ contentEncoding?: string | string[],
46
+ ): AsyncIterable<Uint8Array> | ByteStream;
47
+ export function decodeStream(
48
+ stream: ByteSource,
49
+ contentEncoding?: string | string[],
50
+ ): ByteSource {
51
+ return applyTransforms(stream, contentEncoding, createDecoders);
52
+ }
53
+
54
+ /**
55
+ * Applies encoding transforms for the given content-encoding list.
56
+ *
57
+ * @remarks
58
+ * If no supported encodings are provided, the original source is returned unchanged.
59
+ * Async iterables are converted to Web Streams only when transforms are required.
60
+ */
61
+ export function encodeStream(
62
+ stream: ByteStream,
63
+ contentEncoding?: string | string[],
64
+ ): ByteStream;
65
+ export function encodeStream(
66
+ stream: AsyncIterable<Uint8Array>,
67
+ contentEncoding?: string | string[],
68
+ ): AsyncIterable<Uint8Array> | ByteStream;
69
+ export function encodeStream(
70
+ stream: ByteSource,
71
+ contentEncoding?: string | string[],
72
+ ): ByteSource {
73
+ return applyTransforms(stream, contentEncoding, createEncoders);
74
+ }
75
+
76
+ /**
77
+ * Creates the decoder pipeline for a Content-Encoding or transfer-coding list.
78
+ *
79
+ * @remarks
80
+ * Decoding is applied in reverse order of encoding, as required by HTTP semantics.
81
+ * The special value `identity` is ignored.
82
+ */
83
+ export function createDecoders(
84
+ contentEncoding?: string | string[],
85
+ ): ByteTransform[] {
86
+ const decoders: ByteTransform[] = [];
87
+
88
+ if (contentEncoding?.length) {
89
+ const encodings: string[] = Array.isArray(contentEncoding)
90
+ ? contentEncoding.flatMap(commaSplit)
91
+ : contentEncoding.split(",");
92
+
93
+ for (const encoding of encodings) {
94
+ const normalizedEncoding = normalizeEncoding(encoding);
95
+
96
+ if (normalizedEncoding === "identity") continue;
97
+
98
+ decoders.push(createDecoder(normalizedEncoding));
99
+ }
100
+ }
101
+
102
+ return decoders.reverse();
103
+ }
104
+
105
+ /**
106
+ * Creates the encoder pipeline for a Content-Encoding or transfer-coding list.
107
+ *
108
+ * @remarks
109
+ * Encoders are returned in the declared order. The special value `identity` is ignored.
110
+ */
111
+ export function createEncoders(
112
+ contentEncoding?: string | string[],
113
+ ): ByteTransform[] {
114
+ const encoders: ByteTransform[] = [];
115
+
116
+ if (contentEncoding?.length) {
117
+ const encodings: string[] = Array.isArray(contentEncoding)
118
+ ? contentEncoding.flatMap(commaSplit)
119
+ : contentEncoding.split(",");
120
+
121
+ for (const encoding of encodings) {
122
+ const normalizedEncoding = normalizeEncoding(encoding);
123
+
124
+ if (normalizedEncoding === "identity") continue;
125
+
126
+ encoders.push(createEncoder(normalizedEncoding));
127
+ }
128
+ }
129
+
130
+ return encoders;
131
+ }
132
+
133
+ function commaSplit(header: string): string[] {
134
+ return header.split(",");
135
+ }
136
+
137
+ function normalizeEncoding(encoding: string): string {
138
+ return encoding.trim().toLowerCase();
139
+ }
140
+
141
+ function tagDecodeErrors(native: ByteTransform): ByteTransform {
142
+ const reader = native.readable.getReader();
143
+
144
+ const tagged = new ReadableStream<Uint8Array>({
145
+ pull(controller) {
146
+ return reader.read().then(
147
+ ({ done, value }) => {
148
+ if (done) {
149
+ controller.close();
150
+ } else if (value) {
151
+ controller.enqueue(
152
+ value as unknown as Uint8Array<ArrayBuffer>,
153
+ );
154
+ }
155
+ },
156
+ (err) => {
157
+ controller.error(new DecodeStreamError(err));
158
+ },
159
+ );
160
+ },
161
+ cancel(reason) {
162
+ return reader.cancel(reason);
163
+ },
164
+ });
165
+
166
+ return {
167
+ writable: native.writable,
168
+ readable: tagged,
169
+ } as ByteTransform;
170
+ }
171
+
172
+ function createDecoder(normalizedEncoding: string): ByteTransform {
173
+ switch (normalizedEncoding) {
174
+ case "gzip":
175
+ case "x-gzip":
176
+ return tagDecodeErrors(
177
+ new DecompressionStream("gzip") as ByteTransform,
178
+ );
179
+ case "deflate":
180
+ case "x-deflate":
181
+ return tagDecodeErrors(
182
+ new DecompressionStream("deflate") as ByteTransform,
183
+ );
184
+ case "zstd":
185
+ case "x-zstd":
186
+ return tagDecodeErrors(
187
+ new DecompressionStream("zstd" as any) as ByteTransform,
188
+ );
189
+ case "br":
190
+ return tagDecodeErrors(
191
+ new DecompressionStream("brotli" as any) as ByteTransform,
192
+ );
193
+ case "identity":
194
+ return new TransformStream();
195
+ default:
196
+ throw new TypeError(
197
+ `Unsupported content-encoding: "${normalizedEncoding}"`,
198
+ );
199
+ }
200
+ }
201
+
202
+ function createEncoder(normalizedEncoding: string): ByteTransform {
203
+ switch (normalizedEncoding) {
204
+ case "gzip":
205
+ case "x-gzip":
206
+ return new CompressionStream("gzip") as ByteTransform;
207
+ case "deflate":
208
+ case "x-deflate":
209
+ return new CompressionStream("deflate") as ByteTransform;
210
+ case "zstd":
211
+ case "x-zstd":
212
+ return new CompressionStream("zstd" as any) as ByteTransform;
213
+ case "br":
214
+ return new CompressionStream("brotli" as any) as ByteTransform;
215
+ case "identity":
216
+ return new TransformStream();
217
+ default:
218
+ throw new TypeError(
219
+ `Unsupported content-encoding: "${normalizedEncoding}"`,
220
+ );
221
+ }
222
+ }