@npy/fetch 0.1.0 → 0.1.1
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.
- package/_internal/consts.cjs +4 -0
- package/_internal/consts.js +4 -0
- package/_internal/error-adapters.cjs +146 -0
- package/_internal/error-adapters.js +142 -0
- package/_internal/guards.cjs +24 -0
- package/_internal/guards.js +17 -0
- package/_internal/net.cjs +95 -0
- package/_internal/net.js +92 -0
- package/_internal/promises.cjs +18 -0
- package/_internal/promises.js +18 -0
- package/_internal/streams.cjs +37 -0
- package/_internal/streams.js +36 -0
- package/_virtual/_rolldown/runtime.cjs +23 -0
- package/agent-pool.cjs +78 -0
- package/agent-pool.js +77 -0
- package/agent.cjs +257 -0
- package/agent.js +256 -0
- package/body.cjs +154 -0
- package/body.js +151 -0
- package/dialers/proxy.cjs +49 -0
- package/dialers/proxy.js +48 -0
- package/dialers/tcp.cjs +70 -0
- package/dialers/tcp.js +67 -0
- package/encoding.cjs +95 -0
- package/encoding.js +91 -0
- package/errors.cjs +275 -0
- package/errors.js +259 -0
- package/fetch.cjs +117 -0
- package/fetch.js +115 -0
- package/http-client.cjs +33 -0
- package/http-client.js +33 -0
- package/index.cjs +45 -0
- package/index.d.cts +1 -0
- package/index.d.ts +1 -0
- package/index.js +9 -0
- package/io/_utils.cjs +56 -0
- package/io/_utils.js +51 -0
- package/io/buf-writer.cjs +149 -0
- package/io/buf-writer.js +148 -0
- package/io/io.cjs +135 -0
- package/io/io.js +134 -0
- package/io/readers.cjs +377 -0
- package/io/readers.js +373 -0
- package/io/writers.cjs +191 -0
- package/io/writers.js +190 -0
- package/package.json +7 -10
- package/src/_internal/consts.d.cts +3 -0
- package/src/_internal/consts.d.ts +3 -0
- package/src/_internal/error-adapters.d.cts +22 -0
- package/src/_internal/error-adapters.d.ts +22 -0
- package/src/_internal/guards.d.cts +13 -0
- package/src/_internal/guards.d.ts +13 -0
- package/src/_internal/net.d.cts +12 -0
- package/src/_internal/net.d.ts +12 -0
- package/src/_internal/promises.d.cts +1 -0
- package/src/_internal/promises.d.ts +1 -0
- package/src/_internal/streams.d.cts +21 -0
- package/src/_internal/streams.d.ts +21 -0
- package/src/agent-pool.d.cts +2 -0
- package/src/agent-pool.d.ts +2 -0
- package/src/agent.d.cts +3 -0
- package/src/agent.d.ts +3 -0
- package/src/body.d.cts +23 -0
- package/src/body.d.ts +23 -0
- package/src/dialers/index.d.cts +3 -0
- package/src/dialers/index.d.ts +3 -0
- package/src/dialers/proxy.d.cts +19 -0
- package/src/dialers/proxy.d.ts +19 -0
- package/src/dialers/tcp.d.cts +36 -0
- package/src/dialers/tcp.d.ts +36 -0
- package/src/encoding.d.cts +24 -0
- package/src/encoding.d.ts +24 -0
- package/src/errors.d.cts +110 -0
- package/src/errors.d.ts +110 -0
- package/src/fetch.d.cts +36 -0
- package/src/fetch.d.ts +36 -0
- package/src/http-client.d.cts +23 -0
- package/src/http-client.d.ts +23 -0
- package/src/index.d.cts +7 -0
- package/src/index.d.ts +7 -0
- package/src/io/_utils.d.cts +10 -0
- package/src/io/_utils.d.ts +10 -0
- package/src/io/buf-writer.d.cts +13 -0
- package/src/io/buf-writer.d.ts +13 -0
- package/src/io/io.d.cts +5 -0
- package/src/io/io.d.ts +5 -0
- package/src/io/readers.d.cts +199 -0
- package/src/io/readers.d.ts +199 -0
- package/src/io/writers.d.cts +22 -0
- package/src/io/writers.d.ts +22 -0
- package/src/types/agent.d.cts +128 -0
- package/src/types/agent.d.ts +128 -0
- package/src/types/dialer.d.cts +27 -0
- package/src/types/dialer.d.ts +27 -0
- package/src/types/index.d.cts +2 -0
- package/src/types/index.d.ts +2 -0
- package/tests/test-utils.d.cts +8 -0
- package/tests/test-utils.d.ts +8 -0
package/agent-pool.cjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require("./_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_tcp = require("./dialers/tcp.cjs");
|
|
3
|
+
const require_errors = require("./errors.cjs");
|
|
4
|
+
const require_agent = require("./agent.cjs");
|
|
5
|
+
let generic_pool = require("generic-pool");
|
|
6
|
+
//#region src/agent-pool.ts
|
|
7
|
+
var defaultEvictionInterval = 1e4;
|
|
8
|
+
var defaultMax = Number.MAX_SAFE_INTEGER;
|
|
9
|
+
var defaultIdleTimeout = 3e4;
|
|
10
|
+
function createAgentPool(baseUrl, options = {}) {
|
|
11
|
+
const poolUrl = new URL(baseUrl);
|
|
12
|
+
const evictionRunIntervalMillis = options.poolIdleTimeout !== false ? Math.min(options.poolIdleTimeout || defaultEvictionInterval, defaultEvictionInterval) : 0;
|
|
13
|
+
const max = options.poolMaxPerHost ? Math.max(1, options.poolMaxPerHost) : defaultMax;
|
|
14
|
+
const softIdleTimeoutMillis = options.poolIdleTimeout !== false ? Math.max(1, options.poolIdleTimeout || defaultIdleTimeout) : -1;
|
|
15
|
+
const min = softIdleTimeoutMillis > 0 && options.poolMaxIdlePerHost ? Math.max(0, options.poolMaxIdlePerHost) : 0;
|
|
16
|
+
if (poolUrl.protocol !== "http:" && poolUrl.protocol !== "https:") throw new require_errors.UnsupportedProtocolError(poolUrl.protocol, {
|
|
17
|
+
origin: poolUrl.origin,
|
|
18
|
+
scheme: poolUrl.protocol,
|
|
19
|
+
host: poolUrl.hostname,
|
|
20
|
+
port: poolUrl.port ? Number.parseInt(poolUrl.port, 10) : void 0
|
|
21
|
+
});
|
|
22
|
+
const dialer = options.dialer ?? new require_tcp.AutoDialer();
|
|
23
|
+
const connectOptions = options.connect ?? {};
|
|
24
|
+
const ioOptions = options.io;
|
|
25
|
+
const pool = (0, generic_pool.createPool)({
|
|
26
|
+
async create() {
|
|
27
|
+
return require_agent.createAgent(dialer, baseUrl, {
|
|
28
|
+
connect: connectOptions,
|
|
29
|
+
io: ioOptions
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
async destroy(agent) {
|
|
33
|
+
agent.close();
|
|
34
|
+
}
|
|
35
|
+
}, {
|
|
36
|
+
autostart: false,
|
|
37
|
+
evictionRunIntervalMillis,
|
|
38
|
+
softIdleTimeoutMillis,
|
|
39
|
+
max,
|
|
40
|
+
min
|
|
41
|
+
});
|
|
42
|
+
let releaseAgentFns = [];
|
|
43
|
+
async function send(sendOptions) {
|
|
44
|
+
let agent;
|
|
45
|
+
let agentReleased = false;
|
|
46
|
+
const releaseAgentFn = async (forceClose = false) => {
|
|
47
|
+
if (!agent || agentReleased) return;
|
|
48
|
+
agentReleased = true;
|
|
49
|
+
releaseAgentFns = releaseAgentFns.filter((r) => r !== releaseAgentFn);
|
|
50
|
+
if (forceClose) agent.close();
|
|
51
|
+
if (pool.isBorrowedResource(agent)) await pool.release(agent);
|
|
52
|
+
};
|
|
53
|
+
releaseAgentFns.push(releaseAgentFn);
|
|
54
|
+
try {
|
|
55
|
+
agent = await pool.acquire();
|
|
56
|
+
const responsePromise = agent.send(sendOptions);
|
|
57
|
+
agent.whenIdle().then(() => releaseAgentFn(), () => releaseAgentFn(true));
|
|
58
|
+
return responsePromise;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
await releaseAgentFn(true);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function close() {
|
|
65
|
+
await Promise.all(releaseAgentFns.map((release) => release(true)));
|
|
66
|
+
await pool.drain();
|
|
67
|
+
await pool.clear();
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
[Symbol.asyncDispose]: close,
|
|
71
|
+
close,
|
|
72
|
+
hostname: poolUrl.hostname,
|
|
73
|
+
port: poolUrl.port ? Number.parseInt(poolUrl.port, 10) : poolUrl.protocol === "https:" ? 443 : 80,
|
|
74
|
+
send
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
exports.createAgentPool = createAgentPool;
|
package/agent-pool.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AutoDialer } from "./dialers/tcp.js";
|
|
2
|
+
import { UnsupportedProtocolError } from "./errors.js";
|
|
3
|
+
import { createAgent } from "./agent.js";
|
|
4
|
+
import { createPool } from "generic-pool";
|
|
5
|
+
//#region src/agent-pool.ts
|
|
6
|
+
var defaultEvictionInterval = 1e4;
|
|
7
|
+
var defaultMax = Number.MAX_SAFE_INTEGER;
|
|
8
|
+
var defaultIdleTimeout = 3e4;
|
|
9
|
+
function createAgentPool(baseUrl, options = {}) {
|
|
10
|
+
const poolUrl = new URL(baseUrl);
|
|
11
|
+
const evictionRunIntervalMillis = options.poolIdleTimeout !== false ? Math.min(options.poolIdleTimeout || defaultEvictionInterval, defaultEvictionInterval) : 0;
|
|
12
|
+
const max = options.poolMaxPerHost ? Math.max(1, options.poolMaxPerHost) : defaultMax;
|
|
13
|
+
const softIdleTimeoutMillis = options.poolIdleTimeout !== false ? Math.max(1, options.poolIdleTimeout || defaultIdleTimeout) : -1;
|
|
14
|
+
const min = softIdleTimeoutMillis > 0 && options.poolMaxIdlePerHost ? Math.max(0, options.poolMaxIdlePerHost) : 0;
|
|
15
|
+
if (poolUrl.protocol !== "http:" && poolUrl.protocol !== "https:") throw new UnsupportedProtocolError(poolUrl.protocol, {
|
|
16
|
+
origin: poolUrl.origin,
|
|
17
|
+
scheme: poolUrl.protocol,
|
|
18
|
+
host: poolUrl.hostname,
|
|
19
|
+
port: poolUrl.port ? Number.parseInt(poolUrl.port, 10) : void 0
|
|
20
|
+
});
|
|
21
|
+
const dialer = options.dialer ?? new AutoDialer();
|
|
22
|
+
const connectOptions = options.connect ?? {};
|
|
23
|
+
const ioOptions = options.io;
|
|
24
|
+
const pool = createPool({
|
|
25
|
+
async create() {
|
|
26
|
+
return createAgent(dialer, baseUrl, {
|
|
27
|
+
connect: connectOptions,
|
|
28
|
+
io: ioOptions
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
async destroy(agent) {
|
|
32
|
+
agent.close();
|
|
33
|
+
}
|
|
34
|
+
}, {
|
|
35
|
+
autostart: false,
|
|
36
|
+
evictionRunIntervalMillis,
|
|
37
|
+
softIdleTimeoutMillis,
|
|
38
|
+
max,
|
|
39
|
+
min
|
|
40
|
+
});
|
|
41
|
+
let releaseAgentFns = [];
|
|
42
|
+
async function send(sendOptions) {
|
|
43
|
+
let agent;
|
|
44
|
+
let agentReleased = false;
|
|
45
|
+
const releaseAgentFn = async (forceClose = false) => {
|
|
46
|
+
if (!agent || agentReleased) return;
|
|
47
|
+
agentReleased = true;
|
|
48
|
+
releaseAgentFns = releaseAgentFns.filter((r) => r !== releaseAgentFn);
|
|
49
|
+
if (forceClose) agent.close();
|
|
50
|
+
if (pool.isBorrowedResource(agent)) await pool.release(agent);
|
|
51
|
+
};
|
|
52
|
+
releaseAgentFns.push(releaseAgentFn);
|
|
53
|
+
try {
|
|
54
|
+
agent = await pool.acquire();
|
|
55
|
+
const responsePromise = agent.send(sendOptions);
|
|
56
|
+
agent.whenIdle().then(() => releaseAgentFn(), () => releaseAgentFn(true));
|
|
57
|
+
return responsePromise;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
await releaseAgentFn(true);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function close() {
|
|
64
|
+
await Promise.all(releaseAgentFns.map((release) => release(true)));
|
|
65
|
+
await pool.drain();
|
|
66
|
+
await pool.clear();
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
[Symbol.asyncDispose]: close,
|
|
70
|
+
close,
|
|
71
|
+
hostname: poolUrl.hostname,
|
|
72
|
+
port: poolUrl.port ? Number.parseInt(poolUrl.port, 10) : poolUrl.protocol === "https:" ? 443 : 80,
|
|
73
|
+
send
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createAgentPool };
|
package/agent.cjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
require("./_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_errors = require("./errors.cjs");
|
|
3
|
+
const require_error_adapters = require("./_internal/error-adapters.cjs");
|
|
4
|
+
const require_promises = require("./_internal/promises.cjs");
|
|
5
|
+
const require_io = require("./io/io.cjs");
|
|
6
|
+
let _fuman_utils = require("@fuman/utils");
|
|
7
|
+
//#region src/agent.ts
|
|
8
|
+
var PORT_MAP = {
|
|
9
|
+
"http:": 80,
|
|
10
|
+
"https:": 443
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_ALPN_PROTOCOLS = ["http/1.1"];
|
|
13
|
+
function resolvedDeferred() {
|
|
14
|
+
const deferred = new _fuman_utils.Deferred();
|
|
15
|
+
deferred.resolve();
|
|
16
|
+
return deferred;
|
|
17
|
+
}
|
|
18
|
+
function withSignal(promise, signal) {
|
|
19
|
+
return signal ? require_promises.raceSignal(promise, signal) : promise;
|
|
20
|
+
}
|
|
21
|
+
function isTlsConnection(conn) {
|
|
22
|
+
return "getAlpnProtocol" in conn && typeof conn.getAlpnProtocol === "function";
|
|
23
|
+
}
|
|
24
|
+
function createAgent(dialer, baseUrl, options = {}) {
|
|
25
|
+
const base = new URL(baseUrl);
|
|
26
|
+
if (base.protocol !== "http:" && base.protocol !== "https:") throw new require_errors.UnsupportedProtocolError(base.protocol, {
|
|
27
|
+
origin: base.origin,
|
|
28
|
+
scheme: base.protocol,
|
|
29
|
+
host: base.hostname,
|
|
30
|
+
port: base.port ? Number.parseInt(base.port, 10) : void 0,
|
|
31
|
+
url: base.toString()
|
|
32
|
+
});
|
|
33
|
+
const secure = base.protocol === "https:";
|
|
34
|
+
const hostname = base.hostname;
|
|
35
|
+
const port = base.port ? Number.parseInt(base.port, 10) : PORT_MAP[base.protocol];
|
|
36
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) throw new TypeError(`Invalid port in base URL: ${baseUrl}`);
|
|
37
|
+
const target = {
|
|
38
|
+
address: hostname,
|
|
39
|
+
port,
|
|
40
|
+
secure,
|
|
41
|
+
sni: secure ? hostname : void 0,
|
|
42
|
+
alpnProtocols: secure ? [...DEFAULT_ALPN_PROTOCOLS] : void 0
|
|
43
|
+
};
|
|
44
|
+
const connectOptions = options.connect ?? {};
|
|
45
|
+
const readerOptions = options.io?.reader ?? {};
|
|
46
|
+
const writerOptions = options.io?.writer ?? {};
|
|
47
|
+
let conn;
|
|
48
|
+
let connectPromise;
|
|
49
|
+
let closed = false;
|
|
50
|
+
let isBusy = false;
|
|
51
|
+
let lastUsedTime = Date.now();
|
|
52
|
+
let idleDeferred = resolvedDeferred();
|
|
53
|
+
function createBaseErrorContext() {
|
|
54
|
+
return {
|
|
55
|
+
origin: base.origin,
|
|
56
|
+
scheme: base.protocol,
|
|
57
|
+
host: hostname,
|
|
58
|
+
port
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function createRequestErrorContext(url, method) {
|
|
62
|
+
return {
|
|
63
|
+
...createBaseErrorContext(),
|
|
64
|
+
url: url.toString(),
|
|
65
|
+
method
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function markIdle() {
|
|
69
|
+
isBusy = false;
|
|
70
|
+
lastUsedTime = Date.now();
|
|
71
|
+
idleDeferred.resolve();
|
|
72
|
+
}
|
|
73
|
+
function disposeConn() {
|
|
74
|
+
const current = conn;
|
|
75
|
+
conn = void 0;
|
|
76
|
+
if (!current) return;
|
|
77
|
+
try {
|
|
78
|
+
current.close();
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
function forceClose() {
|
|
82
|
+
if (closed) return;
|
|
83
|
+
closed = true;
|
|
84
|
+
disposeConn();
|
|
85
|
+
if (!isBusy) markIdle();
|
|
86
|
+
}
|
|
87
|
+
function assertUsable() {
|
|
88
|
+
if (closed) throw new require_errors.AgentClosedError(createBaseErrorContext());
|
|
89
|
+
}
|
|
90
|
+
function assertSameOrigin(url) {
|
|
91
|
+
if (url.origin !== base.origin) throw new require_errors.OriginMismatchError(base.origin, url.origin, {
|
|
92
|
+
...createBaseErrorContext(),
|
|
93
|
+
url: url.toString()
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function configureConnection(nextConn) {
|
|
97
|
+
nextConn.setNoDelay(connectOptions.noDelay ?? true);
|
|
98
|
+
if (connectOptions.keepAlive !== null) nextConn.setKeepAlive(connectOptions.keepAlive ?? true);
|
|
99
|
+
}
|
|
100
|
+
async function connect(signal) {
|
|
101
|
+
assertUsable();
|
|
102
|
+
if (conn) return conn;
|
|
103
|
+
if (connectPromise) return withSignal(connectPromise, signal);
|
|
104
|
+
let timedOut = false;
|
|
105
|
+
let timeoutId;
|
|
106
|
+
const abortController = new AbortController();
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
abortController.abort(signal?.reason);
|
|
109
|
+
};
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
if (timeoutId !== void 0) {
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
timeoutId = void 0;
|
|
114
|
+
}
|
|
115
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
116
|
+
};
|
|
117
|
+
if (signal) if (signal.aborted) abortController.abort(signal.reason);
|
|
118
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
119
|
+
if (connectOptions.timeout != null && Number.isFinite(connectOptions.timeout) && connectOptions.timeout > 0) timeoutId = setTimeout(() => {
|
|
120
|
+
timedOut = true;
|
|
121
|
+
abortController.abort(new DOMException("Connection timed out", "TimeoutError"));
|
|
122
|
+
}, connectOptions.timeout);
|
|
123
|
+
connectPromise = (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const nextConn = await dialer.dial(target, { signal: abortController.signal });
|
|
126
|
+
if (closed) {
|
|
127
|
+
try {
|
|
128
|
+
nextConn.close();
|
|
129
|
+
} catch {}
|
|
130
|
+
throw new require_errors.AgentClosedError(createBaseErrorContext());
|
|
131
|
+
}
|
|
132
|
+
configureConnection(nextConn);
|
|
133
|
+
if (secure && isTlsConnection(nextConn)) {
|
|
134
|
+
const alpn = nextConn.getAlpnProtocol();
|
|
135
|
+
if (alpn != null && alpn !== "" && alpn !== "http/1.1") {
|
|
136
|
+
try {
|
|
137
|
+
nextConn.close();
|
|
138
|
+
} catch {}
|
|
139
|
+
throw new require_errors.UnsupportedAlpnProtocolError(alpn, createBaseErrorContext());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
conn = nextConn;
|
|
143
|
+
return nextConn;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw require_error_adapters.mapAdvancedConnectError(error, {
|
|
146
|
+
signal,
|
|
147
|
+
timedOut,
|
|
148
|
+
context: createBaseErrorContext()
|
|
149
|
+
});
|
|
150
|
+
} finally {
|
|
151
|
+
cleanup();
|
|
152
|
+
connectPromise = void 0;
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
155
|
+
return withSignal(connectPromise, signal);
|
|
156
|
+
}
|
|
157
|
+
async function send(sendOptions) {
|
|
158
|
+
assertUsable();
|
|
159
|
+
const url = typeof sendOptions.url === "string" ? new URL(sendOptions.url) : sendOptions.url;
|
|
160
|
+
const method = sendOptions.method.toUpperCase();
|
|
161
|
+
const errorContext = createRequestErrorContext(url, method);
|
|
162
|
+
if (sendOptions.signal?.aborted) throw new require_errors.RequestAbortedError(sendOptions.signal.reason, errorContext);
|
|
163
|
+
if (isBusy) throw new require_errors.AgentBusyError(errorContext);
|
|
164
|
+
assertSameOrigin(url);
|
|
165
|
+
if (method === "CONNECT") throw new require_errors.UnsupportedMethodError("CONNECT", errorContext);
|
|
166
|
+
isBusy = true;
|
|
167
|
+
idleDeferred = new _fuman_utils.Deferred();
|
|
168
|
+
let finalized = false;
|
|
169
|
+
let activeConn;
|
|
170
|
+
const finalize = (reusable) => {
|
|
171
|
+
if (finalized) return;
|
|
172
|
+
finalized = true;
|
|
173
|
+
if (!reusable || closed) {
|
|
174
|
+
if (conn === activeConn) disposeConn();
|
|
175
|
+
else if (activeConn) try {
|
|
176
|
+
activeConn.close();
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
markIdle();
|
|
180
|
+
};
|
|
181
|
+
const abortListener = () => {
|
|
182
|
+
if (activeConn) {
|
|
183
|
+
if (conn === activeConn) conn = void 0;
|
|
184
|
+
try {
|
|
185
|
+
activeConn.close();
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
activeConn = await connect(sendOptions.signal);
|
|
191
|
+
sendOptions.signal?.addEventListener("abort", abortListener, { once: true });
|
|
192
|
+
try {
|
|
193
|
+
await withSignal(require_io.writeRequest(activeConn, {
|
|
194
|
+
url,
|
|
195
|
+
method,
|
|
196
|
+
headers: sendOptions.headers,
|
|
197
|
+
body: sendOptions.body ?? null,
|
|
198
|
+
signal: sendOptions.signal
|
|
199
|
+
}, writerOptions), sendOptions.signal);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw require_error_adapters.mapAdvancedSendError(error, {
|
|
202
|
+
signal: sendOptions.signal,
|
|
203
|
+
context: errorContext,
|
|
204
|
+
phase: "request"
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const isHeadRequest = method === "HEAD";
|
|
208
|
+
const shouldIgnoreBody = (status) => isHeadRequest || status >= 100 && status < 200 || status === 204 || status === 304;
|
|
209
|
+
let response;
|
|
210
|
+
try {
|
|
211
|
+
response = await withSignal(require_io.readResponse(activeConn, readerOptions, shouldIgnoreBody, (reusable) => {
|
|
212
|
+
sendOptions.signal?.removeEventListener("abort", abortListener);
|
|
213
|
+
finalize(reusable);
|
|
214
|
+
}), sendOptions.signal);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw require_error_adapters.mapAdvancedSendError(error, {
|
|
217
|
+
signal: sendOptions.signal,
|
|
218
|
+
context: errorContext,
|
|
219
|
+
phase: "response"
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return require_error_adapters.wrapResponseBodyErrors(response, (error) => require_error_adapters.mapAdvancedSendError(error, {
|
|
223
|
+
signal: sendOptions.signal,
|
|
224
|
+
context: errorContext,
|
|
225
|
+
phase: "body"
|
|
226
|
+
}));
|
|
227
|
+
} catch (error) {
|
|
228
|
+
sendOptions.signal?.removeEventListener("abort", abortListener);
|
|
229
|
+
if (activeConn) {
|
|
230
|
+
if (conn === activeConn) conn = void 0;
|
|
231
|
+
try {
|
|
232
|
+
activeConn.close();
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
235
|
+
finalize(false);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
[Symbol.dispose]: forceClose,
|
|
241
|
+
close: forceClose,
|
|
242
|
+
hostname,
|
|
243
|
+
port,
|
|
244
|
+
send,
|
|
245
|
+
whenIdle() {
|
|
246
|
+
return idleDeferred.promise;
|
|
247
|
+
},
|
|
248
|
+
get isIdle() {
|
|
249
|
+
return !isBusy;
|
|
250
|
+
},
|
|
251
|
+
get lastUsed() {
|
|
252
|
+
return lastUsedTime;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
exports.createAgent = createAgent;
|
package/agent.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { AgentBusyError, AgentClosedError, OriginMismatchError, RequestAbortedError, UnsupportedAlpnProtocolError, UnsupportedMethodError, UnsupportedProtocolError } from "./errors.js";
|
|
2
|
+
import { mapAdvancedConnectError, mapAdvancedSendError, wrapResponseBodyErrors } from "./_internal/error-adapters.js";
|
|
3
|
+
import { raceSignal } from "./_internal/promises.js";
|
|
4
|
+
import { readResponse, writeRequest } from "./io/io.js";
|
|
5
|
+
import { Deferred } from "@fuman/utils";
|
|
6
|
+
//#region src/agent.ts
|
|
7
|
+
var PORT_MAP = {
|
|
8
|
+
"http:": 80,
|
|
9
|
+
"https:": 443
|
|
10
|
+
};
|
|
11
|
+
var DEFAULT_ALPN_PROTOCOLS = ["http/1.1"];
|
|
12
|
+
function resolvedDeferred() {
|
|
13
|
+
const deferred = new Deferred();
|
|
14
|
+
deferred.resolve();
|
|
15
|
+
return deferred;
|
|
16
|
+
}
|
|
17
|
+
function withSignal(promise, signal) {
|
|
18
|
+
return signal ? raceSignal(promise, signal) : promise;
|
|
19
|
+
}
|
|
20
|
+
function isTlsConnection(conn) {
|
|
21
|
+
return "getAlpnProtocol" in conn && typeof conn.getAlpnProtocol === "function";
|
|
22
|
+
}
|
|
23
|
+
function createAgent(dialer, baseUrl, options = {}) {
|
|
24
|
+
const base = new URL(baseUrl);
|
|
25
|
+
if (base.protocol !== "http:" && base.protocol !== "https:") throw new UnsupportedProtocolError(base.protocol, {
|
|
26
|
+
origin: base.origin,
|
|
27
|
+
scheme: base.protocol,
|
|
28
|
+
host: base.hostname,
|
|
29
|
+
port: base.port ? Number.parseInt(base.port, 10) : void 0,
|
|
30
|
+
url: base.toString()
|
|
31
|
+
});
|
|
32
|
+
const secure = base.protocol === "https:";
|
|
33
|
+
const hostname = base.hostname;
|
|
34
|
+
const port = base.port ? Number.parseInt(base.port, 10) : PORT_MAP[base.protocol];
|
|
35
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) throw new TypeError(`Invalid port in base URL: ${baseUrl}`);
|
|
36
|
+
const target = {
|
|
37
|
+
address: hostname,
|
|
38
|
+
port,
|
|
39
|
+
secure,
|
|
40
|
+
sni: secure ? hostname : void 0,
|
|
41
|
+
alpnProtocols: secure ? [...DEFAULT_ALPN_PROTOCOLS] : void 0
|
|
42
|
+
};
|
|
43
|
+
const connectOptions = options.connect ?? {};
|
|
44
|
+
const readerOptions = options.io?.reader ?? {};
|
|
45
|
+
const writerOptions = options.io?.writer ?? {};
|
|
46
|
+
let conn;
|
|
47
|
+
let connectPromise;
|
|
48
|
+
let closed = false;
|
|
49
|
+
let isBusy = false;
|
|
50
|
+
let lastUsedTime = Date.now();
|
|
51
|
+
let idleDeferred = resolvedDeferred();
|
|
52
|
+
function createBaseErrorContext() {
|
|
53
|
+
return {
|
|
54
|
+
origin: base.origin,
|
|
55
|
+
scheme: base.protocol,
|
|
56
|
+
host: hostname,
|
|
57
|
+
port
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createRequestErrorContext(url, method) {
|
|
61
|
+
return {
|
|
62
|
+
...createBaseErrorContext(),
|
|
63
|
+
url: url.toString(),
|
|
64
|
+
method
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function markIdle() {
|
|
68
|
+
isBusy = false;
|
|
69
|
+
lastUsedTime = Date.now();
|
|
70
|
+
idleDeferred.resolve();
|
|
71
|
+
}
|
|
72
|
+
function disposeConn() {
|
|
73
|
+
const current = conn;
|
|
74
|
+
conn = void 0;
|
|
75
|
+
if (!current) return;
|
|
76
|
+
try {
|
|
77
|
+
current.close();
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
function forceClose() {
|
|
81
|
+
if (closed) return;
|
|
82
|
+
closed = true;
|
|
83
|
+
disposeConn();
|
|
84
|
+
if (!isBusy) markIdle();
|
|
85
|
+
}
|
|
86
|
+
function assertUsable() {
|
|
87
|
+
if (closed) throw new AgentClosedError(createBaseErrorContext());
|
|
88
|
+
}
|
|
89
|
+
function assertSameOrigin(url) {
|
|
90
|
+
if (url.origin !== base.origin) throw new OriginMismatchError(base.origin, url.origin, {
|
|
91
|
+
...createBaseErrorContext(),
|
|
92
|
+
url: url.toString()
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function configureConnection(nextConn) {
|
|
96
|
+
nextConn.setNoDelay(connectOptions.noDelay ?? true);
|
|
97
|
+
if (connectOptions.keepAlive !== null) nextConn.setKeepAlive(connectOptions.keepAlive ?? true);
|
|
98
|
+
}
|
|
99
|
+
async function connect(signal) {
|
|
100
|
+
assertUsable();
|
|
101
|
+
if (conn) return conn;
|
|
102
|
+
if (connectPromise) return withSignal(connectPromise, signal);
|
|
103
|
+
let timedOut = false;
|
|
104
|
+
let timeoutId;
|
|
105
|
+
const abortController = new AbortController();
|
|
106
|
+
const onAbort = () => {
|
|
107
|
+
abortController.abort(signal?.reason);
|
|
108
|
+
};
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
if (timeoutId !== void 0) {
|
|
111
|
+
clearTimeout(timeoutId);
|
|
112
|
+
timeoutId = void 0;
|
|
113
|
+
}
|
|
114
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
115
|
+
};
|
|
116
|
+
if (signal) if (signal.aborted) abortController.abort(signal.reason);
|
|
117
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
118
|
+
if (connectOptions.timeout != null && Number.isFinite(connectOptions.timeout) && connectOptions.timeout > 0) timeoutId = setTimeout(() => {
|
|
119
|
+
timedOut = true;
|
|
120
|
+
abortController.abort(new DOMException("Connection timed out", "TimeoutError"));
|
|
121
|
+
}, connectOptions.timeout);
|
|
122
|
+
connectPromise = (async () => {
|
|
123
|
+
try {
|
|
124
|
+
const nextConn = await dialer.dial(target, { signal: abortController.signal });
|
|
125
|
+
if (closed) {
|
|
126
|
+
try {
|
|
127
|
+
nextConn.close();
|
|
128
|
+
} catch {}
|
|
129
|
+
throw new AgentClosedError(createBaseErrorContext());
|
|
130
|
+
}
|
|
131
|
+
configureConnection(nextConn);
|
|
132
|
+
if (secure && isTlsConnection(nextConn)) {
|
|
133
|
+
const alpn = nextConn.getAlpnProtocol();
|
|
134
|
+
if (alpn != null && alpn !== "" && alpn !== "http/1.1") {
|
|
135
|
+
try {
|
|
136
|
+
nextConn.close();
|
|
137
|
+
} catch {}
|
|
138
|
+
throw new UnsupportedAlpnProtocolError(alpn, createBaseErrorContext());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
conn = nextConn;
|
|
142
|
+
return nextConn;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw mapAdvancedConnectError(error, {
|
|
145
|
+
signal,
|
|
146
|
+
timedOut,
|
|
147
|
+
context: createBaseErrorContext()
|
|
148
|
+
});
|
|
149
|
+
} finally {
|
|
150
|
+
cleanup();
|
|
151
|
+
connectPromise = void 0;
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
return withSignal(connectPromise, signal);
|
|
155
|
+
}
|
|
156
|
+
async function send(sendOptions) {
|
|
157
|
+
assertUsable();
|
|
158
|
+
const url = typeof sendOptions.url === "string" ? new URL(sendOptions.url) : sendOptions.url;
|
|
159
|
+
const method = sendOptions.method.toUpperCase();
|
|
160
|
+
const errorContext = createRequestErrorContext(url, method);
|
|
161
|
+
if (sendOptions.signal?.aborted) throw new RequestAbortedError(sendOptions.signal.reason, errorContext);
|
|
162
|
+
if (isBusy) throw new AgentBusyError(errorContext);
|
|
163
|
+
assertSameOrigin(url);
|
|
164
|
+
if (method === "CONNECT") throw new UnsupportedMethodError("CONNECT", errorContext);
|
|
165
|
+
isBusy = true;
|
|
166
|
+
idleDeferred = new Deferred();
|
|
167
|
+
let finalized = false;
|
|
168
|
+
let activeConn;
|
|
169
|
+
const finalize = (reusable) => {
|
|
170
|
+
if (finalized) return;
|
|
171
|
+
finalized = true;
|
|
172
|
+
if (!reusable || closed) {
|
|
173
|
+
if (conn === activeConn) disposeConn();
|
|
174
|
+
else if (activeConn) try {
|
|
175
|
+
activeConn.close();
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
markIdle();
|
|
179
|
+
};
|
|
180
|
+
const abortListener = () => {
|
|
181
|
+
if (activeConn) {
|
|
182
|
+
if (conn === activeConn) conn = void 0;
|
|
183
|
+
try {
|
|
184
|
+
activeConn.close();
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
activeConn = await connect(sendOptions.signal);
|
|
190
|
+
sendOptions.signal?.addEventListener("abort", abortListener, { once: true });
|
|
191
|
+
try {
|
|
192
|
+
await withSignal(writeRequest(activeConn, {
|
|
193
|
+
url,
|
|
194
|
+
method,
|
|
195
|
+
headers: sendOptions.headers,
|
|
196
|
+
body: sendOptions.body ?? null,
|
|
197
|
+
signal: sendOptions.signal
|
|
198
|
+
}, writerOptions), sendOptions.signal);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw mapAdvancedSendError(error, {
|
|
201
|
+
signal: sendOptions.signal,
|
|
202
|
+
context: errorContext,
|
|
203
|
+
phase: "request"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const isHeadRequest = method === "HEAD";
|
|
207
|
+
const shouldIgnoreBody = (status) => isHeadRequest || status >= 100 && status < 200 || status === 204 || status === 304;
|
|
208
|
+
let response;
|
|
209
|
+
try {
|
|
210
|
+
response = await withSignal(readResponse(activeConn, readerOptions, shouldIgnoreBody, (reusable) => {
|
|
211
|
+
sendOptions.signal?.removeEventListener("abort", abortListener);
|
|
212
|
+
finalize(reusable);
|
|
213
|
+
}), sendOptions.signal);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw mapAdvancedSendError(error, {
|
|
216
|
+
signal: sendOptions.signal,
|
|
217
|
+
context: errorContext,
|
|
218
|
+
phase: "response"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return wrapResponseBodyErrors(response, (error) => mapAdvancedSendError(error, {
|
|
222
|
+
signal: sendOptions.signal,
|
|
223
|
+
context: errorContext,
|
|
224
|
+
phase: "body"
|
|
225
|
+
}));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
sendOptions.signal?.removeEventListener("abort", abortListener);
|
|
228
|
+
if (activeConn) {
|
|
229
|
+
if (conn === activeConn) conn = void 0;
|
|
230
|
+
try {
|
|
231
|
+
activeConn.close();
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
finalize(false);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
[Symbol.dispose]: forceClose,
|
|
240
|
+
close: forceClose,
|
|
241
|
+
hostname,
|
|
242
|
+
port,
|
|
243
|
+
send,
|
|
244
|
+
whenIdle() {
|
|
245
|
+
return idleDeferred.promise;
|
|
246
|
+
},
|
|
247
|
+
get isIdle() {
|
|
248
|
+
return !isBusy;
|
|
249
|
+
},
|
|
250
|
+
get lastUsed() {
|
|
251
|
+
return lastUsedTime;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
export { createAgent };
|