@npy/fetch 0.1.3 → 0.1.4

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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/_internal/consts.cjs +4 -0
  3. package/_internal/consts.d.cts +3 -0
  4. package/_internal/consts.d.ts +3 -0
  5. package/_internal/consts.js +4 -0
  6. package/_internal/decode-stream-error.cjs +18 -0
  7. package/{src/_internal/decode-stream-error.ts → _internal/decode-stream-error.d.cts} +2 -7
  8. package/_internal/decode-stream-error.d.ts +11 -0
  9. package/_internal/decode-stream-error.js +18 -0
  10. package/_internal/error-mapping.cjs +44 -0
  11. package/_internal/error-mapping.d.cts +15 -0
  12. package/_internal/error-mapping.d.ts +15 -0
  13. package/_internal/error-mapping.js +41 -0
  14. package/_internal/guards.cjs +23 -0
  15. package/_internal/guards.d.cts +15 -0
  16. package/_internal/guards.d.ts +15 -0
  17. package/_internal/guards.js +15 -0
  18. package/_internal/net.cjs +95 -0
  19. package/_internal/net.d.cts +11 -0
  20. package/_internal/net.d.ts +11 -0
  21. package/_internal/net.js +92 -0
  22. package/_internal/promises.cjs +18 -0
  23. package/_internal/promises.d.cts +1 -0
  24. package/_internal/promises.d.ts +1 -0
  25. package/_internal/promises.js +18 -0
  26. package/_internal/streams.cjs +37 -0
  27. package/_internal/streams.d.cts +21 -0
  28. package/_internal/streams.d.ts +21 -0
  29. package/_internal/streams.js +36 -0
  30. package/_internal/symbols.cjs +4 -0
  31. package/_internal/symbols.d.cts +1 -0
  32. package/_internal/symbols.d.ts +1 -0
  33. package/_internal/symbols.js +4 -0
  34. package/_virtual/_rolldown/runtime.cjs +23 -0
  35. package/agent-pool.cjs +96 -0
  36. package/agent-pool.d.cts +2 -0
  37. package/agent-pool.d.ts +2 -0
  38. package/agent-pool.js +95 -0
  39. package/agent.cjs +260 -0
  40. package/agent.d.cts +3 -0
  41. package/agent.d.ts +3 -0
  42. package/agent.js +259 -0
  43. package/body.cjs +105 -0
  44. package/body.d.cts +12 -0
  45. package/body.d.ts +12 -0
  46. package/body.js +102 -0
  47. package/dialers/index.d.cts +3 -0
  48. package/dialers/index.d.ts +3 -0
  49. package/dialers/proxy.cjs +56 -0
  50. package/dialers/proxy.d.cts +27 -0
  51. package/dialers/proxy.d.ts +27 -0
  52. package/dialers/proxy.js +55 -0
  53. package/dialers/tcp.cjs +92 -0
  54. package/dialers/tcp.d.cts +57 -0
  55. package/dialers/tcp.d.ts +57 -0
  56. package/dialers/tcp.js +89 -0
  57. package/encoding.cjs +114 -0
  58. package/encoding.d.cts +35 -0
  59. package/encoding.d.ts +35 -0
  60. package/encoding.js +110 -0
  61. package/errors.cjs +275 -0
  62. package/errors.d.cts +110 -0
  63. package/errors.d.ts +110 -0
  64. package/errors.js +259 -0
  65. package/fetch.cjs +353 -0
  66. package/fetch.d.cts +58 -0
  67. package/fetch.d.ts +58 -0
  68. package/fetch.js +350 -0
  69. package/http-client.cjs +75 -0
  70. package/http-client.d.cts +39 -0
  71. package/http-client.d.ts +39 -0
  72. package/http-client.js +75 -0
  73. package/index.cjs +49 -0
  74. package/index.d.cts +14 -0
  75. package/index.d.ts +14 -0
  76. package/index.js +11 -0
  77. package/io/_utils.cjs +56 -0
  78. package/io/_utils.d.cts +10 -0
  79. package/io/_utils.d.ts +10 -0
  80. package/io/_utils.js +51 -0
  81. package/io/buf-writer.cjs +149 -0
  82. package/io/buf-writer.d.cts +13 -0
  83. package/io/buf-writer.d.ts +13 -0
  84. package/io/buf-writer.js +148 -0
  85. package/io/io.cjs +199 -0
  86. package/io/io.d.cts +5 -0
  87. package/io/io.d.ts +5 -0
  88. package/io/io.js +198 -0
  89. package/io/readers.cjs +337 -0
  90. package/io/readers.d.cts +69 -0
  91. package/io/readers.d.ts +69 -0
  92. package/io/readers.js +333 -0
  93. package/io/writers.cjs +196 -0
  94. package/io/writers.d.cts +22 -0
  95. package/io/writers.d.ts +22 -0
  96. package/io/writers.js +195 -0
  97. package/package.json +30 -25
  98. package/{src/types/agent.ts → types/agent.d.cts} +21 -47
  99. package/types/agent.d.ts +72 -0
  100. package/{src/types/dialer.ts → types/dialer.d.cts} +9 -19
  101. package/types/dialer.d.ts +30 -0
  102. package/types/index.d.cts +2 -0
  103. package/types/index.d.ts +2 -0
  104. package/bun.lock +0 -68
  105. package/examples/custom-proxy-client.ts +0 -32
  106. package/examples/http-client.ts +0 -47
  107. package/examples/proxy.ts +0 -16
  108. package/examples/simple.ts +0 -15
  109. package/src/_internal/consts.ts +0 -3
  110. package/src/_internal/error-mapping.ts +0 -160
  111. package/src/_internal/guards.ts +0 -78
  112. package/src/_internal/net.ts +0 -173
  113. package/src/_internal/promises.ts +0 -22
  114. package/src/_internal/streams.ts +0 -52
  115. package/src/_internal/symbols.ts +0 -1
  116. package/src/agent-pool.ts +0 -157
  117. package/src/agent.ts +0 -408
  118. package/src/body.ts +0 -179
  119. package/src/dialers/index.ts +0 -3
  120. package/src/dialers/proxy.ts +0 -102
  121. package/src/dialers/tcp.ts +0 -162
  122. package/src/encoding.ts +0 -222
  123. package/src/errors.ts +0 -357
  124. package/src/fetch.ts +0 -626
  125. package/src/http-client.ts +0 -111
  126. package/src/index.ts +0 -14
  127. package/src/io/_utils.ts +0 -82
  128. package/src/io/buf-writer.ts +0 -183
  129. package/src/io/io.ts +0 -322
  130. package/src/io/readers.ts +0 -576
  131. package/src/io/writers.ts +0 -331
  132. package/src/types/index.ts +0 -2
  133. package/tests/agent-pool.test.ts +0 -111
  134. package/tests/agent.test.ts +0 -134
  135. package/tests/body.test.ts +0 -228
  136. package/tests/errors.test.ts +0 -152
  137. package/tests/fetch.test.ts +0 -421
  138. package/tests/io-options.test.ts +0 -127
  139. package/tests/multipart.test.ts +0 -348
  140. package/tests/test-utils.ts +0 -335
  141. package/tsconfig.json +0 -15
package/src/body.ts DELETED
@@ -1,179 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import { Readable } from "node:stream";
3
- import { isAnyArrayBuffer } from "node:util/types";
4
- import type { IClosable, IReadable } from "@fuman/io";
5
- import { webReadableToFuman } from "@fuman/io";
6
- import { utf8 } from "@fuman/utils";
7
- import { CRLF_LENGTH, CRLF_STR } from "./_internal/consts";
8
- import {
9
- type FormDataPolyfill,
10
- isBlob,
11
- isFormData,
12
- isFumanReadable,
13
- isIterable,
14
- isMultipartFormDataStream,
15
- isReadable,
16
- isReadableStream,
17
- isURLSearchParameters,
18
- } from "./_internal/guards";
19
-
20
- export type BodyInit =
21
- | Exclude<RequestInit["body"], undefined | null>
22
- | FormDataPolyfill
23
- | Readable
24
- | (IReadable & IClosable);
25
-
26
- export interface BodyState {
27
- contentLength: number | null;
28
- contentType: string | null;
29
-
30
- body:
31
- | Readable
32
- | ReadableStream
33
- | Uint8Array
34
- | (IReadable & IClosable)
35
- | null;
36
- }
37
-
38
- type Bytes = Uint8Array<ArrayBufferLike>;
39
-
40
- const BOUNDARY = "-".repeat(2);
41
-
42
- const makeFormBoundary = (): string =>
43
- `formdata-${randomBytes(8).toString("hex")}`;
44
-
45
- const getFormHeader = (
46
- boundary: string,
47
- name: string,
48
- field: File | Blob | string,
49
- ): string => {
50
- let header = `${BOUNDARY}${boundary}${CRLF_STR}`;
51
- header += `Content-Disposition: form-data; name="${name}"`;
52
- if (isBlob(field)) {
53
- header += `; filename="${(field as File).name ?? "blob"}"${CRLF_STR}`;
54
- header += `Content-Type: ${field.type || "application/octet-stream"}`;
55
- }
56
- return `${header}${CRLF_STR}${CRLF_STR}`;
57
- };
58
-
59
- const getFormFooter = (boundary: string) =>
60
- `${BOUNDARY}${boundary}${BOUNDARY}${CRLF_STR}${CRLF_STR}`;
61
-
62
- export const getFormDataLength = (form: FormData, boundary: string) => {
63
- let length = Buffer.byteLength(getFormFooter(boundary));
64
- for (const [name, value] of form)
65
- length +=
66
- Buffer.byteLength(getFormHeader(boundary, name, value)) +
67
- (isBlob(value) ? value.size : Buffer.byteLength(`${value}`)) +
68
- CRLF_LENGTH;
69
- return length;
70
- };
71
-
72
- async function* generatorOfFormData(
73
- form: FormData,
74
- boundary: string,
75
- ): AsyncGenerator<Bytes> {
76
- for (const [name, value] of form) {
77
- if (isBlob(value)) {
78
- yield utf8.encoder.encode(
79
- getFormHeader(boundary, name, value),
80
- ) as Bytes;
81
-
82
- for await (const chunk of value.stream() as any as AsyncIterable<Bytes>) {
83
- yield chunk;
84
- }
85
-
86
- yield utf8.encoder.encode(CRLF_STR) as Bytes;
87
- } else {
88
- yield utf8.encoder.encode(
89
- getFormHeader(boundary, name, value) + value + CRLF_STR,
90
- ) as Bytes;
91
- }
92
- }
93
- yield utf8.encoder.encode(getFormFooter(boundary)) as Bytes;
94
- }
95
-
96
- export const extractBody = (object: BodyInit | null): BodyState => {
97
- let type: string | null = null;
98
- let body:
99
- | Readable
100
- | ReadableStream
101
- | Uint8Array
102
- | (IReadable & IClosable)
103
- | null;
104
- let size: number | null = null;
105
-
106
- if (object == null) {
107
- body = null;
108
- size = 0;
109
- } else if (typeof object === "string") {
110
- const bytes = utf8.encoder.encode(`${object}`);
111
- type = "text/plain;charset=UTF-8";
112
- size = bytes.byteLength;
113
- body = bytes;
114
- } else if (isURLSearchParameters(object)) {
115
- const bytes = utf8.encoder.encode(object.toString());
116
- body = bytes;
117
- size = bytes.byteLength;
118
- type = "application/x-www-form-urlencoded;charset=UTF-8";
119
- } else if (isBlob(object)) {
120
- size = object.size;
121
- type = object.type || null;
122
- body = object.stream();
123
- } else if (object instanceof Uint8Array) {
124
- body = object;
125
- size = object.byteLength;
126
- } else if (isAnyArrayBuffer(object)) {
127
- const bytes = new Uint8Array(object);
128
- body = bytes;
129
- size = bytes.byteLength;
130
- } else if (ArrayBuffer.isView(object)) {
131
- const bytes = new Uint8Array(
132
- object.buffer,
133
- object.byteOffset,
134
- object.byteLength,
135
- );
136
- body = bytes;
137
- size = bytes.byteLength;
138
- } else if (isReadableStream(object)) {
139
- body = object;
140
- } else if (isFumanReadable(object)) {
141
- body = object;
142
- } else if (isFormData(object)) {
143
- const boundary = makeFormBoundary();
144
- type = `multipart/form-data; boundary=${boundary}`;
145
- size = getFormDataLength(object, boundary);
146
- body = Readable.from(generatorOfFormData(object, boundary));
147
- } else if (isMultipartFormDataStream(object)) {
148
- type = `multipart/form-data; boundary=${object.getBoundary()}`;
149
- size = object.hasKnownLength() ? object.getLengthSync() : null;
150
- body = object as Readable;
151
- } else if (isReadable(object)) {
152
- body = object as Readable;
153
- } else if (isIterable(object)) {
154
- body = Readable.from(object);
155
- } else {
156
- const bytes = utf8.encoder.encode(`${object}`);
157
- type = "text/plain;charset=UTF-8";
158
- body = bytes;
159
- size = bytes.byteLength;
160
- }
161
-
162
- return {
163
- contentLength: size,
164
- contentType: type,
165
- body,
166
- };
167
- };
168
-
169
- export function fromRequestBody(request: Request): BodyInit | null {
170
- if (request.bodyUsed) {
171
- throw new TypeError("Request body has already been used");
172
- }
173
-
174
- if (request.body == null) {
175
- return null;
176
- }
177
-
178
- return webReadableToFuman(request.body);
179
- }
@@ -1,3 +0,0 @@
1
- export type { ConnectionLike, Dialer, DialTarget } from "../types/dialer";
2
- export * from "./proxy";
3
- export * from "./tcp";
@@ -1,102 +0,0 @@
1
- import type { ITcpConnection } from "@fuman/net";
2
- import type { NodeTlsUpgradeOptions } from "@fuman/node";
3
- import {
4
- createProxyConnection,
5
- type ProxyConnectionFn,
6
- type ProxyInfo,
7
- parse as parseProxy,
8
- } from "@npy/proxy-kit";
9
- import { connectTcp, upgradeTls } from "../_internal/net";
10
- import type { Dialer } from "../types/dialer";
11
-
12
- const DEFAULT_HTTP_ALPN_PROTOCOLS = ["http/1.1"] as const;
13
-
14
- type ProxyConnectOptions = Parameters<typeof connectTcp>[0];
15
- type UpgradableTcpConnection = Parameters<typeof upgradeTls>[0];
16
- type ResolvedProxy = Parameters<typeof createProxyConnection>[0]["proxy"];
17
-
18
- function normalizeProxy(proxy: ProxyDialer.Input): ResolvedProxy {
19
- if (typeof proxy !== "string") {
20
- return proxy;
21
- }
22
-
23
- const parsed = parseProxy(proxy, { strict: true });
24
- if (parsed == null) {
25
- throw new TypeError(`Invalid proxy string: ${proxy}`);
26
- }
27
-
28
- return parsed;
29
- }
30
-
31
- /**
32
- * Dialer that routes connections through an HTTP, HTTPS or SOCKS proxy.
33
- *
34
- * @remarks
35
- * Secure targets are tunneled and then upgraded to TLS after the proxy connection
36
- * has been established.
37
- */
38
- export class ProxyDialer implements Dialer {
39
- readonly proxy: ResolvedProxy;
40
- readonly #options: Readonly<ProxyDialer.Options>;
41
- readonly #connectThroughProxy: ProxyConnectionFn<ProxyConnectOptions>;
42
-
43
- constructor(proxy: ProxyDialer.Input, options: ProxyDialer.Options = {}) {
44
- this.proxy = normalizeProxy(proxy);
45
- this.#options = { ...options };
46
- this.#connectThroughProxy = createProxyConnection({
47
- proxy: this.proxy,
48
- connectionFn: connectTcp,
49
- });
50
- }
51
-
52
- async dial(
53
- target: Dialer.Target,
54
- options: Dialer.Options = {},
55
- ): Promise<Dialer.ConnectionLike> {
56
- const tunneled = await this.#connectThroughProxy({
57
- address: target.address,
58
- port: target.port,
59
- signal: options.signal,
60
- });
61
-
62
- if (!target.secure) {
63
- return tunneled;
64
- }
65
-
66
- return this.#upgradeSecureTarget(tunneled, target, options.signal);
67
- }
68
-
69
- async #upgradeSecureTarget(
70
- conn: ITcpConnection,
71
- target: Dialer.Target,
72
- signal?: AbortSignal,
73
- ): Promise<Dialer.ConnectionLike> {
74
- const sni = target.sni ?? this.#options.sni ?? target.address;
75
- const extraOptions =
76
- this.#options.extraOptions || target.extraOptions
77
- ? { ...this.#options.extraOptions, ...target.extraOptions }
78
- : undefined;
79
-
80
- const tlsOptions: NodeTlsUpgradeOptions & { signal?: AbortSignal } = {
81
- signal,
82
- caCerts: this.#options.caCerts,
83
- sni,
84
- alpnProtocols: target.alpnProtocols ??
85
- this.#options.alpnProtocols ?? [...DEFAULT_HTTP_ALPN_PROTOCOLS],
86
- extraOptions,
87
- };
88
-
89
- return upgradeTls(conn as UpgradableTcpConnection, tlsOptions);
90
- }
91
- }
92
-
93
- export namespace ProxyDialer {
94
- export type Input = string | ProxyInfo;
95
-
96
- export interface Options {
97
- caCerts?: string[];
98
- sni?: string;
99
- alpnProtocols?: string[];
100
- extraOptions?: NodeTlsUpgradeOptions["extraOptions"];
101
- }
102
- }
@@ -1,162 +0,0 @@
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
- }
package/src/encoding.ts DELETED
@@ -1,222 +0,0 @@
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
- }