gerdur-core 1.0.3 → 1.0.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.
- package/CHANGELOG.md +12 -0
- package/dist/api/request.js +66 -45
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/lib/get-url.d.ts +21 -0
- package/dist/lib/get-url.js +91 -46
- package/dist/lib/http.d.ts +5 -1
- package/dist/lib/http.js +24 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.4 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `resolveDownloadUrls(tracks, qualities)` — resolve download URLs for a whole album/playlist in **one** `media.deezer.com/v1/get_url` POST (`track_tokens[]` + an ordered `formats[]` fallback), instead of one request per track re-tried per quality. ~19× faster URL resolution for a 14-track album. Returns one `ResolvedUrl | null` per input track, in order. `formatName(quality)` is also exported.
|
|
8
|
+
- `httpAgent` / `httpsAgent` — the shared keep-alive connection pools are now exported so downstream apps can hand them to `got`/`undici` and reuse the same sockets for downloads. `getBuffer` / `getJson` / `getText` are exported too.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Shared HTTP agents are tuned and bounded**: `keepAlive`, `maxSockets: 64` (was the Node default of `Infinity`), `maxFreeSockets: 16`, `scheduling: 'lifo'`, 30 s keep-alive. Caps a pathological fan-out from opening hundreds of sockets to `api.deezer.com`, while leaving generous headroom over the converter's concurrency.
|
|
13
|
+
- **In-flight request coalescing** in the API layer: concurrent identical gateway / public-API calls (e.g. every track of one album asking for the same album info while tagging) now share a single request instead of each missing the still-empty cache and hitting the wire.
|
|
14
|
+
|
|
3
15
|
## 1.0.3 - 2026-08-30
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/dist/api/request.js
CHANGED
|
@@ -6,6 +6,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.requestPublicApi = exports.requestGet = exports.requestLight = exports.request = void 0;
|
|
7
7
|
const request_1 = __importDefault(require("../lib/request"));
|
|
8
8
|
const cache_1 = __importDefault(require("./cache"));
|
|
9
|
+
/**
|
|
10
|
+
* In-flight request coalescing (single-flight).
|
|
11
|
+
*
|
|
12
|
+
* The LRU only helps *after* a response lands. During a batch download the
|
|
13
|
+
* pipeline fires many identical metadata calls at once — e.g. tagging 14 tracks
|
|
14
|
+
* of one album triggers 14 concurrent `album/<id>` lookups — and every one of
|
|
15
|
+
* them misses the still-empty cache and hits the network. Here a second caller
|
|
16
|
+
* for a key already in flight awaits the same promise instead, so the wire sees
|
|
17
|
+
* exactly one request. Entries are removed as soon as they settle; the LRU takes
|
|
18
|
+
* over from there.
|
|
19
|
+
*/
|
|
20
|
+
const inFlight = new Map();
|
|
21
|
+
const coalesce = (cacheKey, fetcher) => {
|
|
22
|
+
const cached = cache_1.default.get(cacheKey);
|
|
23
|
+
if (cached) {
|
|
24
|
+
return Promise.resolve(cached);
|
|
25
|
+
}
|
|
26
|
+
const pending = inFlight.get(cacheKey);
|
|
27
|
+
if (pending) {
|
|
28
|
+
return pending;
|
|
29
|
+
}
|
|
30
|
+
const promise = (async () => {
|
|
31
|
+
try {
|
|
32
|
+
return await fetcher();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
inFlight.delete(cacheKey);
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
inFlight.set(cacheKey, promise);
|
|
39
|
+
return promise;
|
|
40
|
+
};
|
|
9
41
|
/**
|
|
10
42
|
* Make POST requests to deezer api
|
|
11
43
|
* @param {Object} body post body
|
|
@@ -13,17 +45,14 @@ const cache_1 = __importDefault(require("./cache"));
|
|
|
13
45
|
*/
|
|
14
46
|
const request = async (body, method) => {
|
|
15
47
|
const cacheKey = method + ':' + Object.entries(body).join(':');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
const errorMessage = Object.entries(error).join(', ');
|
|
26
|
-
throw new Error(errorMessage);
|
|
48
|
+
return coalesce(cacheKey, async () => {
|
|
49
|
+
const { data: { error, results }, } = await request_1.default.post('/gateway.php', body, { params: { method } });
|
|
50
|
+
if (Object.keys(results).length > 0) {
|
|
51
|
+
cache_1.default.set(cacheKey, results);
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
throw new Error(Object.entries(error).join(', '));
|
|
55
|
+
});
|
|
27
56
|
};
|
|
28
57
|
exports.request = request;
|
|
29
58
|
/**
|
|
@@ -33,19 +62,16 @@ exports.request = request;
|
|
|
33
62
|
*/
|
|
34
63
|
const requestLight = async (body, method) => {
|
|
35
64
|
const cacheKey = method + ':' + Object.entries(body).join(':');
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
return coalesce(cacheKey, async () => {
|
|
66
|
+
const { data: { error, results }, } = await request_1.default.post('https://www.deezer.com/ajax/gw-light.php', body, {
|
|
67
|
+
params: { method, api_version: '1.0' },
|
|
68
|
+
});
|
|
69
|
+
if (Object.keys(results).length > 0) {
|
|
70
|
+
cache_1.default.set(cacheKey, results);
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(Object.entries(error).join(', '));
|
|
42
74
|
});
|
|
43
|
-
if (Object.keys(results).length > 0) {
|
|
44
|
-
cache_1.default.set(cacheKey, results);
|
|
45
|
-
return results;
|
|
46
|
-
}
|
|
47
|
-
const errorMessage = Object.entries(error).join(', ');
|
|
48
|
-
throw new Error(errorMessage);
|
|
49
75
|
};
|
|
50
76
|
exports.requestLight = requestLight;
|
|
51
77
|
/**
|
|
@@ -55,17 +81,14 @@ exports.requestLight = requestLight;
|
|
|
55
81
|
*/
|
|
56
82
|
const requestGet = async (method, params = {}, key = 'get_request') => {
|
|
57
83
|
const cacheKey = method + key;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
const errorMessage = Object.entries(error).join(', ');
|
|
68
|
-
throw new Error(errorMessage);
|
|
84
|
+
return coalesce(cacheKey, async () => {
|
|
85
|
+
const { data: { error, results }, } = await request_1.default.get('/gateway.php', { params: { method, ...params } });
|
|
86
|
+
if (Object.keys(results).length > 0) {
|
|
87
|
+
cache_1.default.set(cacheKey, results);
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(Object.entries(error).join(', '));
|
|
91
|
+
});
|
|
69
92
|
};
|
|
70
93
|
exports.requestGet = requestGet;
|
|
71
94
|
/**
|
|
@@ -73,16 +96,14 @@ exports.requestGet = requestGet;
|
|
|
73
96
|
* @param {String} slug endpoint
|
|
74
97
|
*/
|
|
75
98
|
const requestPublicApi = async (slug) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
cache_1.default.set(slug, data);
|
|
86
|
-
return data;
|
|
99
|
+
return coalesce(slug, async () => {
|
|
100
|
+
const { data } = await request_1.default.get('https://api.deezer.com' + slug);
|
|
101
|
+
if (data.error) {
|
|
102
|
+
const errorMessage = Object.entries(data.error).join(', ');
|
|
103
|
+
throw new Error(errorMessage);
|
|
104
|
+
}
|
|
105
|
+
cache_1.default.set(slug, data);
|
|
106
|
+
return data;
|
|
107
|
+
});
|
|
87
108
|
};
|
|
88
109
|
exports.requestPublicApi = requestPublicApi;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,11 +14,17 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.initDeezerApi = void 0;
|
|
17
|
+
exports.getText = exports.getJson = exports.getBuffer = exports.httpsAgent = exports.httpAgent = exports.initDeezerApi = void 0;
|
|
18
18
|
var request_1 = require("./lib/request");
|
|
19
19
|
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return request_1.initDeezerApi; } });
|
|
20
20
|
__exportStar(require("./api"), exports);
|
|
21
21
|
__exportStar(require("./converter"), exports);
|
|
22
22
|
__exportStar(require("./lib/decrypt"), exports);
|
|
23
23
|
__exportStar(require("./lib/get-url"), exports);
|
|
24
|
+
var http_1 = require("./lib/http");
|
|
25
|
+
Object.defineProperty(exports, "httpAgent", { enumerable: true, get: function () { return http_1.httpAgent; } });
|
|
26
|
+
Object.defineProperty(exports, "httpsAgent", { enumerable: true, get: function () { return http_1.httpsAgent; } });
|
|
27
|
+
Object.defineProperty(exports, "getBuffer", { enumerable: true, get: function () { return http_1.getBuffer; } });
|
|
28
|
+
Object.defineProperty(exports, "getJson", { enumerable: true, get: function () { return http_1.getJson; } });
|
|
29
|
+
Object.defineProperty(exports, "getText", { enumerable: true, get: function () { return http_1.getText; } });
|
|
24
30
|
__exportStar(require("./metadata-writer"), exports);
|
package/dist/lib/get-url.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class ExpiredTrackToken extends Error {
|
|
|
13
13
|
readonly sngId: string;
|
|
14
14
|
constructor(sngId: string);
|
|
15
15
|
}
|
|
16
|
+
/** quality code -> the `format` string the media API expects */
|
|
17
|
+
export declare const formatName: (quality: number) => string;
|
|
16
18
|
/**
|
|
17
19
|
* @param track Track info json returned from `getTrackInfo`
|
|
18
20
|
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
|
@@ -22,3 +24,22 @@ export declare const getTrackDownloadUrl: (track: trackType, quality: number) =>
|
|
|
22
24
|
isEncrypted: boolean;
|
|
23
25
|
fileSize: number;
|
|
24
26
|
} | null>;
|
|
27
|
+
export interface ResolvedUrl {
|
|
28
|
+
trackUrl: string;
|
|
29
|
+
isEncrypted: boolean;
|
|
30
|
+
fileSize: number;
|
|
31
|
+
/** the format Deezer actually returned, e.g. `'FLAC'` */
|
|
32
|
+
format: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve download URLs for many tracks in a **single** `get_url` request.
|
|
36
|
+
*
|
|
37
|
+
* `qualities` is an ordered preference list (e.g. `[9, 3, 1]`): Deezer returns
|
|
38
|
+
* the best each track is licensed for, so there is no per-quality retry. The
|
|
39
|
+
* result has one entry per input track, in order; `null` for a track that is
|
|
40
|
+
* geo-blocked, unavailable, or errored.
|
|
41
|
+
*
|
|
42
|
+
* @param tracks from `getTrackInfo` / `parseInfo` (needs `TRACK_TOKEN`, `SNG_ID`, `FILESIZE_*`)
|
|
43
|
+
* @param qualities preference order — 9 = FLAC, 3 = MP3 320, 1 = MP3 128
|
|
44
|
+
*/
|
|
45
|
+
export declare const resolveDownloadUrls: (tracks: trackType[], qualities?: number[]) => Promise<(ResolvedUrl | null)[]>;
|
package/dist/lib/get-url.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getTrackDownloadUrl = exports.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
6
|
+
exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.formatName = exports.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
7
7
|
const delay_1 = __importDefault(require("delay"));
|
|
8
8
|
const decrypt_1 = require("../lib/decrypt");
|
|
9
9
|
const http_1 = require("../lib/http");
|
|
@@ -66,49 +66,68 @@ const dzAuthenticate = async () => {
|
|
|
66
66
|
return user_data;
|
|
67
67
|
};
|
|
68
68
|
const MEDIA_MAX_RETRIES = 3;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
/** quality code -> the `format` string the media API expects */
|
|
70
|
+
const formatName = (quality) => {
|
|
71
|
+
switch (quality) {
|
|
72
|
+
case 9:
|
|
73
|
+
return 'FLAC';
|
|
74
|
+
case 3:
|
|
75
|
+
return 'MP3_320';
|
|
76
|
+
case 1:
|
|
77
|
+
return 'MP3_128';
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`Unknown quality ${quality}`);
|
|
73
80
|
}
|
|
74
|
-
|
|
81
|
+
};
|
|
82
|
+
exports.formatName = formatName;
|
|
83
|
+
/** POST media.deezer.com/v1/get_url with re-auth + exponential-backoff retry on 403/429/5xx. */
|
|
84
|
+
const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
|
|
85
|
+
var _a;
|
|
86
|
+
const user = user_data ? user_data : await dzAuthenticate();
|
|
75
87
|
try {
|
|
76
|
-
|
|
88
|
+
const { data } = await request_1.default.post('https://media.deezer.com/v1/get_url', {
|
|
77
89
|
license_token: user.license_token,
|
|
78
|
-
media: [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
track_tokens: [track_token],
|
|
85
|
-
}));
|
|
90
|
+
media: [{ type: 'FULL', formats }],
|
|
91
|
+
track_tokens,
|
|
92
|
+
});
|
|
93
|
+
return { data: (_a = data.data) !== null && _a !== void 0 ? _a : [], country: user.country };
|
|
86
94
|
}
|
|
87
95
|
catch (err) {
|
|
88
|
-
// media.deezer.com throttles / 5xx / stale license_token — re-auth and back off
|
|
89
96
|
const status = err instanceof http_1.HttpStatusError ? err.statusCode : 0;
|
|
90
97
|
if ((status === 403 || status === 429 || status >= 500) && attempt < MEDIA_MAX_RETRIES) {
|
|
91
98
|
user_data = null;
|
|
92
99
|
await (0, delay_1.default)(500 * 2 ** attempt + Math.floor(Math.random() * 250));
|
|
93
|
-
return
|
|
100
|
+
return mediaGetUrl(track_tokens, formats, attempt + 1);
|
|
94
101
|
}
|
|
95
102
|
throw err;
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
};
|
|
105
|
+
/** Parse one `data[i]` entry from a get_url response into a source URL (or null / throw). */
|
|
106
|
+
const parseMediaEntry = (entry, token, country) => {
|
|
107
|
+
var _a, _b;
|
|
108
|
+
if (entry === null || entry === void 0 ? void 0 : entry.errors) {
|
|
109
|
+
const { code } = entry.errors[0];
|
|
110
|
+
if (code === 2002)
|
|
111
|
+
throw new GeoBlocked(country);
|
|
112
|
+
if (code === 2000 || code === 2001)
|
|
113
|
+
throw new ExpiredTrackToken(token);
|
|
114
|
+
throw new Error(Object.entries(entry.errors[0]).join(', '));
|
|
115
|
+
}
|
|
116
|
+
const media = (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a[0];
|
|
117
|
+
if (!((_b = media === null || media === void 0 ? void 0 : media.sources) === null || _b === void 0 ? void 0 : _b.length))
|
|
118
|
+
return null;
|
|
119
|
+
return { url: media.sources[0].url, format: media.format };
|
|
120
|
+
};
|
|
121
|
+
const getTrackUrlFromServer = async (track_token, format) => {
|
|
122
|
+
var _a, _b;
|
|
123
|
+
const user = user_data ? user_data : await dzAuthenticate();
|
|
124
|
+
if ((format === 'FLAC' && !user.can_stream_lossless) || (format === 'MP3_320' && !user.can_stream_hq)) {
|
|
125
|
+
throw new WrongLicense(format);
|
|
110
126
|
}
|
|
111
|
-
|
|
127
|
+
const { data, country } = await mediaGetUrl([track_token], [{ format, cipher: 'BF_CBC_STRIPE' }]);
|
|
128
|
+
if (!data.length)
|
|
129
|
+
return null;
|
|
130
|
+
return (_b = (_a = parseMediaEntry(data[0], track_token, country)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : null;
|
|
112
131
|
};
|
|
113
132
|
/**
|
|
114
133
|
* @param track Track info json returned from `getTrackInfo`
|
|
@@ -119,20 +138,7 @@ const getTrackDownloadUrl = async (track, quality) => {
|
|
|
119
138
|
let geoBlocked = null;
|
|
120
139
|
let expiredToken = null;
|
|
121
140
|
let mediaBlocked = null;
|
|
122
|
-
|
|
123
|
-
switch (quality) {
|
|
124
|
-
case 9:
|
|
125
|
-
formatName = 'FLAC';
|
|
126
|
-
break;
|
|
127
|
-
case 3:
|
|
128
|
-
formatName = 'MP3_320';
|
|
129
|
-
break;
|
|
130
|
-
case 1:
|
|
131
|
-
formatName = 'MP3_128';
|
|
132
|
-
break;
|
|
133
|
-
default:
|
|
134
|
-
throw new Error(`Unknown quality ${quality}`);
|
|
135
|
-
}
|
|
141
|
+
const format = (0, exports.formatName)(quality);
|
|
136
142
|
// Track tokens last ~1 hour. If it's already stale, skip the doomed media API
|
|
137
143
|
// call and go straight to the token-free fallback below.
|
|
138
144
|
const tokenStale = Boolean(track.TRACK_TOKEN_EXPIRE && track.TRACK_TOKEN_EXPIRE * 1000 < Date.now());
|
|
@@ -142,7 +148,7 @@ const getTrackDownloadUrl = async (track, quality) => {
|
|
|
142
148
|
else {
|
|
143
149
|
// Get URL with the official API
|
|
144
150
|
try {
|
|
145
|
-
const url = await getTrackUrlFromServer(track.TRACK_TOKEN,
|
|
151
|
+
const url = await getTrackUrlFromServer(track.TRACK_TOKEN, format);
|
|
146
152
|
if (url) {
|
|
147
153
|
return {
|
|
148
154
|
trackUrl: url,
|
|
@@ -207,3 +213,42 @@ const testUrl = async (url) => {
|
|
|
207
213
|
return 0;
|
|
208
214
|
}
|
|
209
215
|
};
|
|
216
|
+
const QUALITY_OF_FORMAT = { FLAC: 9, MP3_320: 3, MP3_128: 1, MP3_256: 3, MP3_64: 1 };
|
|
217
|
+
/**
|
|
218
|
+
* Resolve download URLs for many tracks in a **single** `get_url` request.
|
|
219
|
+
*
|
|
220
|
+
* `qualities` is an ordered preference list (e.g. `[9, 3, 1]`): Deezer returns
|
|
221
|
+
* the best each track is licensed for, so there is no per-quality retry. The
|
|
222
|
+
* result has one entry per input track, in order; `null` for a track that is
|
|
223
|
+
* geo-blocked, unavailable, or errored.
|
|
224
|
+
*
|
|
225
|
+
* @param tracks from `getTrackInfo` / `parseInfo` (needs `TRACK_TOKEN`, `SNG_ID`, `FILESIZE_*`)
|
|
226
|
+
* @param qualities preference order — 9 = FLAC, 3 = MP3 320, 1 = MP3 128
|
|
227
|
+
*/
|
|
228
|
+
const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
|
|
229
|
+
if (!tracks.length)
|
|
230
|
+
return [];
|
|
231
|
+
const formats = qualities.map((q) => ({ format: (0, exports.formatName)(q), cipher: 'BF_CBC_STRIPE' }));
|
|
232
|
+
const { data, country } = await mediaGetUrl(tracks.map((t) => t.TRACK_TOKEN), formats);
|
|
233
|
+
return tracks.map((track, i) => {
|
|
234
|
+
var _a;
|
|
235
|
+
let parsed;
|
|
236
|
+
try {
|
|
237
|
+
parsed = parseMediaEntry(data[i], track.TRACK_TOKEN, country);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// per-track error (geo-block, no rights, expired token) — skip this one
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (!parsed)
|
|
244
|
+
return null;
|
|
245
|
+
const q = (_a = QUALITY_OF_FORMAT[parsed.format]) !== null && _a !== void 0 ? _a : qualities[0];
|
|
246
|
+
return {
|
|
247
|
+
trackUrl: parsed.url,
|
|
248
|
+
isEncrypted: parsed.url.includes('/mobile/') || parsed.url.includes('/media/'),
|
|
249
|
+
fileSize: getTrackFileSize(track, q),
|
|
250
|
+
format: parsed.format,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
exports.resolveDownloadUrls = resolveDownloadUrls;
|
package/dist/lib/http.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
import { Agent as HttpAgent, IncomingHttpHeaders } from 'http';
|
|
5
|
+
import { Agent as HttpsAgent } from 'https';
|
|
4
6
|
type HttpMethod = 'GET' | 'POST' | 'HEAD';
|
|
5
7
|
type ResponseType = 'buffer' | 'json' | 'text';
|
|
6
8
|
type QueryValue = string | number | boolean | null | undefined;
|
|
@@ -22,6 +24,8 @@ interface RequestDescriptor {
|
|
|
22
24
|
timeout: number;
|
|
23
25
|
url: string;
|
|
24
26
|
}
|
|
27
|
+
export declare const httpAgent: HttpAgent;
|
|
28
|
+
export declare const httpsAgent: HttpsAgent;
|
|
25
29
|
export interface HttpResponse<T> {
|
|
26
30
|
config: RequestDescriptor;
|
|
27
31
|
data: T;
|
package/dist/lib/http.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.headRequest = exports.getText = exports.getJson = exports.getBuffer = exports.HttpClient = exports.HttpStatusError = void 0;
|
|
3
|
+
exports.headRequest = exports.getText = exports.getJson = exports.getBuffer = exports.HttpClient = exports.HttpStatusError = exports.httpsAgent = exports.httpAgent = void 0;
|
|
4
4
|
const http_1 = require("http");
|
|
5
5
|
const https_1 = require("https");
|
|
6
6
|
const zlib_1 = require("zlib");
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Shared keep-alive connection pools. Every request gerdur-core makes — gateway,
|
|
9
|
+
* media API, CDN HEAD probes — reuses these, so a batch of calls to the same host
|
|
10
|
+
* rides one handful of TCP+TLS connections instead of a fresh handshake each time.
|
|
11
|
+
*
|
|
12
|
+
* `maxSockets` is bounded (not the Node default of `Infinity`): the converter fans
|
|
13
|
+
* out at concurrency 10–25, so 64 is generous headroom while still capping a
|
|
14
|
+
* pathological fan-out that would otherwise open hundreds of sockets to
|
|
15
|
+
* `api.deezer.com` and get the account rate-limited. `scheduling: 'lifo'` keeps
|
|
16
|
+
* the hottest socket warm. Downstream apps can import these and hand them to
|
|
17
|
+
* `got`/`undici` so downloads share the same pool.
|
|
18
|
+
*/
|
|
19
|
+
const AGENT_OPTS = {
|
|
20
|
+
keepAlive: true,
|
|
21
|
+
keepAliveMsecs: 30000,
|
|
22
|
+
maxSockets: 64,
|
|
23
|
+
maxFreeSockets: 16,
|
|
24
|
+
scheduling: 'lifo',
|
|
25
|
+
timeout: 60000,
|
|
26
|
+
};
|
|
27
|
+
exports.httpAgent = new http_1.Agent(AGENT_OPTS);
|
|
28
|
+
exports.httpsAgent = new https_1.Agent(AGENT_OPTS);
|
|
9
29
|
class HttpStatusError extends Error {
|
|
10
30
|
constructor(statusCode, headers, body) {
|
|
11
31
|
super(`Request failed with status code ${statusCode}`);
|
|
@@ -86,7 +106,7 @@ const requestRaw = async (config, redirectCount = 0) => {
|
|
|
86
106
|
const isHttps = requestUrl.protocol === 'https:';
|
|
87
107
|
const requestFn = isHttps ? https_1.request : http_1.request;
|
|
88
108
|
const req = requestFn({
|
|
89
|
-
agent: isHttps ? httpsAgent : httpAgent,
|
|
109
|
+
agent: isHttps ? exports.httpsAgent : exports.httpAgent,
|
|
90
110
|
headers,
|
|
91
111
|
hostname: requestUrl.hostname,
|
|
92
112
|
method: config.method,
|