jsonseo 1.0.0
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/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/cjs/client.d.ts +71 -0
- package/dist/cjs/client.js +135 -0
- package/dist/cjs/common.d.ts +40 -0
- package/dist/cjs/common.js +2 -0
- package/dist/cjs/errors.d.ts +71 -0
- package/dist/cjs/errors.js +123 -0
- package/dist/cjs/http.d.ts +77 -0
- package/dist/cjs/http.js +307 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/params.d.ts +361 -0
- package/dist/cjs/params.js +2 -0
- package/dist/cjs/responses.d.ts +352 -0
- package/dist/cjs/responses.js +2 -0
- package/dist/esm/client.d.ts +71 -0
- package/dist/esm/client.js +131 -0
- package/dist/esm/common.d.ts +40 -0
- package/dist/esm/common.js +1 -0
- package/dist/esm/errors.d.ts +71 -0
- package/dist/esm/errors.js +106 -0
- package/dist/esm/http.d.ts +77 -0
- package/dist/esm/http.js +302 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/params.d.ts +361 -0
- package/dist/esm/params.js +1 -0
- package/dist/esm/responses.d.ts +352 -0
- package/dist/esm/responses.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsonSeoClient = void 0;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
const http_js_1 = require("./http.js");
|
|
6
|
+
/**
|
|
7
|
+
* Клиент JSON SEO API.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const client = new JsonSeoClient('ВАШ_КЛЮЧ');
|
|
11
|
+
* const serp = await client.yandex({ text: 'купить ноутбук', region: 213 });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
class JsonSeoClient {
|
|
15
|
+
constructor(first, second = {}) {
|
|
16
|
+
this.http = new http_js_1.HttpClient(typeof first === 'string' ? { ...second, apiKey: first } : first);
|
|
17
|
+
}
|
|
18
|
+
// Яндекс
|
|
19
|
+
/** Органическая выдача Яндекса: мобильная, регион 213. 0.01 ₽ за страницу. */
|
|
20
|
+
async yandex(params, options) {
|
|
21
|
+
return this.http.json('yandex', primary(params, 'text'), options);
|
|
22
|
+
}
|
|
23
|
+
/** Подсказки Яндекса: до 50 фраз с учётом региона. 0.01 ₽ за запрос. */
|
|
24
|
+
async yandexSuggest(params, options) {
|
|
25
|
+
return this.http.json('yandex/suggest', primary(params, 'text'), options);
|
|
26
|
+
}
|
|
27
|
+
/** Код региона (lr) по названию города или области. Бесплатно, нужен ключ. */
|
|
28
|
+
async yandexRegions(params, options) {
|
|
29
|
+
return this.http.json('yandex/regions', primary(params, 'name'), options);
|
|
30
|
+
}
|
|
31
|
+
/** Картинки Яндекса: 20 карточек на страницу, 0.01 ₽ за страницу. */
|
|
32
|
+
async yandexImages(params, options) {
|
|
33
|
+
return this.http.json('yandex/images', primary(params, 'q'), options);
|
|
34
|
+
}
|
|
35
|
+
/** Видео Яндекса: 20 карточек на страницу, 0.01 ₽ за страницу. */
|
|
36
|
+
async yandexVideo(params, options) {
|
|
37
|
+
return this.http.json('yandex/video', primary(params, 'q'), options);
|
|
38
|
+
}
|
|
39
|
+
// Google
|
|
40
|
+
/** Органическая выдача google.com: мобильная, 0.01 ₽ за страницу. */
|
|
41
|
+
async google(params, options) {
|
|
42
|
+
return this.http.json('google', primary(params, 'q'), options);
|
|
43
|
+
}
|
|
44
|
+
/** Подсказки Google: до ~15 фраз. 0.01 ₽ за запрос. */
|
|
45
|
+
async googleSuggest(params, options) {
|
|
46
|
+
return this.http.json('google/suggest', primary(params, 'q'), options);
|
|
47
|
+
}
|
|
48
|
+
/** ID региона Google по названию и готовый `uule`. Бесплатно, нужен ключ. */
|
|
49
|
+
async googleRegions(params, options) {
|
|
50
|
+
return this.http.json('google/regions', primary(params, 'name'), options);
|
|
51
|
+
}
|
|
52
|
+
/** Картинки Google: 100 карточек на страницу, 0.01 ₽ за страницу. */
|
|
53
|
+
async googleImages(params, options) {
|
|
54
|
+
return this.http.json('google/images', primary(params, 'q'), options);
|
|
55
|
+
}
|
|
56
|
+
/** Видео Google: 10 карточек на страницу, 0.01 ₽ за страницу. */
|
|
57
|
+
async googleVideo(params, options) {
|
|
58
|
+
return this.http.json('google/video', primary(params, 'q'), options);
|
|
59
|
+
}
|
|
60
|
+
// Bing
|
|
61
|
+
/** Органическая выдача bing.com: без локации — Россия, 0.01 ₽ за страницу. */
|
|
62
|
+
async bing(params, options) {
|
|
63
|
+
return this.http.json('bing', primary(params, 'q'), options);
|
|
64
|
+
}
|
|
65
|
+
/** Подсказки Bing. 0.01 ₽ за запрос. */
|
|
66
|
+
async bingSuggest(params, options) {
|
|
67
|
+
return this.http.json('bing/suggest', primary(params, 'q'), options);
|
|
68
|
+
}
|
|
69
|
+
/** Картинки Bing: `count` карточек (по умолчанию 35), дальше 700-й не листает. */
|
|
70
|
+
async bingImages(params, options) {
|
|
71
|
+
return this.http.json('bing/images', primary(params, 'q'), options);
|
|
72
|
+
}
|
|
73
|
+
/** Видео Bing: `count` карточек на страницу, по умолчанию 105. */
|
|
74
|
+
async bingVideo(params, options) {
|
|
75
|
+
return this.http.json('bing/video', primary(params, 'q'), options);
|
|
76
|
+
}
|
|
77
|
+
// Вордстат
|
|
78
|
+
/** Популярные и похожие запросы. 0.01 ₽ за запрос. */
|
|
79
|
+
async wordstat(params, options) {
|
|
80
|
+
return this.http.json('wordstat', primary(params, 'text'), options);
|
|
81
|
+
}
|
|
82
|
+
/** Частота запроса одним числом — `results.totalValue`. 0.01 ₽ за запрос. */
|
|
83
|
+
async wordstatFrequency(params, options) {
|
|
84
|
+
return this.http.json('wordstat/frequency', primary(params, 'text'), options);
|
|
85
|
+
}
|
|
86
|
+
/** Динамика показов по месяцам, неделям или дням. 0.01 ₽ за запрос. */
|
|
87
|
+
async wordstatGraph(params, options) {
|
|
88
|
+
return this.http.json('wordstat/graph', primary(params, 'text'), options);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Показы по регионам и городам. `popularity` — affinity-индекс: 100 —
|
|
92
|
+
* средний интерес. 0.01 ₽ за запрос.
|
|
93
|
+
*/
|
|
94
|
+
async wordstatMap(params, options) {
|
|
95
|
+
return this.http.json('wordstat/map', primary(params, 'text'), options);
|
|
96
|
+
}
|
|
97
|
+
// Директ и служебные методы
|
|
98
|
+
/**
|
|
99
|
+
* Прогноз показов Директа со ставками и бюджетом. Кабинет не нужен.
|
|
100
|
+
*
|
|
101
|
+
* 0.01 ₽ за пачку до 4000 символов (около 150 фраз). До 1000 фраз за
|
|
102
|
+
* запрос, 100 запросов в час.
|
|
103
|
+
*/
|
|
104
|
+
async direct(params, options) {
|
|
105
|
+
return this.http.json('direct', primary(params, 'phrases'), options);
|
|
106
|
+
}
|
|
107
|
+
/** Страна, регион и координаты по IPv4. Бесплатно, нужен ключ. */
|
|
108
|
+
async geoip(params, options) {
|
|
109
|
+
return this.http.json('geoip', primary(params, 'ip'), options);
|
|
110
|
+
}
|
|
111
|
+
/** Текущий баланс. Бесплатно, нужен ключ. */
|
|
112
|
+
async balance(options) {
|
|
113
|
+
return this.http.json('balance', {}, options);
|
|
114
|
+
}
|
|
115
|
+
// Запасной выход
|
|
116
|
+
/** Произвольный метод API — если в сервисе появился новый. */
|
|
117
|
+
async call(path, params = {}, options) {
|
|
118
|
+
return this.http.json(path, params, options);
|
|
119
|
+
}
|
|
120
|
+
/** То же, но ответ возвращается строкой без разбора. */
|
|
121
|
+
async callRaw(path, params = {}, options) {
|
|
122
|
+
return this.http.text(path, params, options);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.JsonSeoClient = JsonSeoClient;
|
|
126
|
+
/** Даёт вызывать метод строкой или, для `direct()`, списком фраз. */
|
|
127
|
+
function primary(params, key) {
|
|
128
|
+
if (typeof params === 'string' || Array.isArray(params)) {
|
|
129
|
+
return { [key]: params };
|
|
130
|
+
}
|
|
131
|
+
if (params === null || typeof params !== 'object') {
|
|
132
|
+
throw new errors_js_1.InvalidArgumentError('Параметры метода передаются строкой, массивом или объектом.');
|
|
133
|
+
}
|
|
134
|
+
return params;
|
|
135
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Значение параметра: массив склеивается, флаг даёт 1/0, null не уедет. */
|
|
2
|
+
export type ParamValue = string | number | boolean | ReadonlyArray<string | number> | null | undefined;
|
|
3
|
+
/** Переключатель: API принимает и 1/0, и true/false. */
|
|
4
|
+
export type Flag = boolean | 0 | 1;
|
|
5
|
+
/** Устройство, с которого снимается выдача. */
|
|
6
|
+
export type Device = 'mobile' | 'desktop' | 'tablet';
|
|
7
|
+
/** У Google и Bing планшетной выдачи нет. */
|
|
8
|
+
export type MobileDevice = 'mobile' | 'desktop';
|
|
9
|
+
/** Фильтрация взрослого контента у Яндекса. */
|
|
10
|
+
export type AdultFilter = 'none' | 'moderate' | 'strict';
|
|
11
|
+
/** Она же у Bing — значения называются иначе. */
|
|
12
|
+
export type BingSafeSearch = 'off' | 'moderate' | 'strict';
|
|
13
|
+
/** SafeSearch Google. */
|
|
14
|
+
export type GoogleSafeSearch = 'active' | 'off';
|
|
15
|
+
/** Доменная зона Яндекса. */
|
|
16
|
+
export type YandexZone = 'ru' | 'tr' | 'com' | 'kz' | 'by' | 'be' | 'kk' | 'uz' | 'com.tr';
|
|
17
|
+
/** Язык названий в справочниках регионов. */
|
|
18
|
+
export type RegionLanguage = 'ru' | 'en';
|
|
19
|
+
/** Размер картинки. У Google меньшая ступень — это значки ровно 256 px. */
|
|
20
|
+
export type ImageSize = 'large' | 'medium' | 'small';
|
|
21
|
+
/** Ориентация картинки. Есть у всех трёх поисковиков. */
|
|
22
|
+
export type ImageOrientation = 'horizontal' | 'vertical' | 'square';
|
|
23
|
+
/** Цвет: color — полноцветные, mono — чёрно-белые, остальное — основной. */
|
|
24
|
+
export type ImageColor = 'color' | 'mono' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'blue' | 'purple' | 'white' | 'black' | 'pink' | 'brown';
|
|
25
|
+
/** Тип изображения: transparent нет у Яндекса, demotivator есть только у него. */
|
|
26
|
+
export type ImageType = 'photo' | 'clipart' | 'lineart' | 'face' | 'animated' | 'transparent' | 'demotivator';
|
|
27
|
+
/** Формат файла. У Bing поддерживается только gif. */
|
|
28
|
+
export type ImageFormat = 'jpg' | 'png' | 'gif';
|
|
29
|
+
/** Насколько свежий результат. Окна у поисковиков свои. */
|
|
30
|
+
export type Freshness = 'day' | 'week' | 'month' | 'year';
|
|
31
|
+
/** Длительность ролика. Границы ступеней у каждого поисковика свои. */
|
|
32
|
+
export type VideoDuration = 'short' | 'medium' | 'long';
|
|
33
|
+
/** Вид частотности: операторы расставит сервис, кавычки не нужны. */
|
|
34
|
+
export type WordstatKind = 'base' | 'phrase' | 'exact' | 'superexact';
|
|
35
|
+
/** Шаг динамики: month и week — история с 2018 года, day — последние 60 дней. */
|
|
36
|
+
export type WordstatGraphType = 'day' | 'week' | 'month';
|
|
37
|
+
/** Разрез географии показов. */
|
|
38
|
+
export type WordstatMapType = 'all' | 'regions' | 'cities';
|
|
39
|
+
/** Период прогноза Яндекс Директа. */
|
|
40
|
+
export type DirectPeriod = 'week' | 'month' | 'quarter' | 'year';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Общий предок всех ошибок SDK. */
|
|
2
|
+
export declare class JsonSeoError extends Error {
|
|
3
|
+
constructor(message: string, options?: {
|
|
4
|
+
cause?: unknown;
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
/** Сервис ответил отказом. Тело сохраняется целиком. */
|
|
8
|
+
export declare class JsonSeoApiError extends JsonSeoError {
|
|
9
|
+
/** HTTP-статус ответа. */
|
|
10
|
+
readonly status: number;
|
|
11
|
+
/** Тело ответа как есть. */
|
|
12
|
+
readonly body: string;
|
|
13
|
+
/** Разобранный JSON ответа. Пустой объект, если тело не разобралось. */
|
|
14
|
+
readonly payload: Record<string, unknown>;
|
|
15
|
+
/** Через сколько секунд вернуться. null, если срок не назван. */
|
|
16
|
+
readonly retryAfter: number | null;
|
|
17
|
+
constructor(message: string, status: number, body: string, payload: Record<string, unknown>, retryAfter?: number | null);
|
|
18
|
+
}
|
|
19
|
+
/** 402: на счёте не хватает средств. */
|
|
20
|
+
export declare class PaymentRequiredError extends JsonSeoApiError {
|
|
21
|
+
}
|
|
22
|
+
/** 403 или 401: ключ не передан или недействителен. */
|
|
23
|
+
export declare class UnauthorizedError extends JsonSeoApiError {
|
|
24
|
+
}
|
|
25
|
+
/** 503: выдачу получить не вышло. Деньги не списаны, повтор обычно проходит. */
|
|
26
|
+
export declare class ServiceUnavailableError extends JsonSeoApiError {
|
|
27
|
+
}
|
|
28
|
+
/** 422: параметры запроса не приняты. Деньги не списываются. */
|
|
29
|
+
export declare class ValidationError extends JsonSeoApiError {
|
|
30
|
+
/** Ошибки по именам параметров: `{ text: ['Введите запрос'] }`. */
|
|
31
|
+
get errors(): Record<string, string[]>;
|
|
32
|
+
/** Забракованные параметры. */
|
|
33
|
+
get fields(): string[];
|
|
34
|
+
}
|
|
35
|
+
/** 429: превышен лимит частоты. Срок повтора — в `retryAfter`. */
|
|
36
|
+
export declare class RateLimitError extends JsonSeoApiError {
|
|
37
|
+
}
|
|
38
|
+
/** До сервиса не достучались: сеть, DNS, TLS. Статуса нет. */
|
|
39
|
+
export declare class NetworkError extends JsonSeoError {
|
|
40
|
+
constructor(message: string, cause?: unknown);
|
|
41
|
+
}
|
|
42
|
+
/** Запрос не уложился в отведённое время и был прерван на стороне клиента. */
|
|
43
|
+
export declare class TimeoutError extends NetworkError {
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Заголовки пришли, а тело дочитать не вышло. Автоматически не повторяется:
|
|
47
|
+
* выдача уже собрана и оплачена, обрыв случился на отдаче.
|
|
48
|
+
*/
|
|
49
|
+
export declare class IncompleteResponseError extends NetworkError {
|
|
50
|
+
}
|
|
51
|
+
/** Запрос прерван переданным в него AbortSignal. */
|
|
52
|
+
export declare class AbortError extends JsonSeoError {
|
|
53
|
+
}
|
|
54
|
+
/** Запрос не отправлен: SDK забраковал аргументы ещё до обращения к сети. */
|
|
55
|
+
export declare class InvalidArgumentError extends JsonSeoError {
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Успех, но тело не разобралось как JSON. Тело сохраняется: страница уже
|
|
59
|
+
* оплачена, и достать из неё данные руками лучше, чем не иметь ничего.
|
|
60
|
+
*/
|
|
61
|
+
export declare class ParseError extends JsonSeoError {
|
|
62
|
+
/** Тело ответа как есть. */
|
|
63
|
+
readonly body: string;
|
|
64
|
+
constructor(message: string, body: string);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Собирает ошибку под этот HTTP-статус.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export declare function apiErrorFor(status: number, body: string, payload: Record<string, unknown>, retryAfter: number | null): JsonSeoApiError;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ParseError = exports.InvalidArgumentError = exports.AbortError = exports.IncompleteResponseError = exports.TimeoutError = exports.NetworkError = exports.RateLimitError = exports.ValidationError = exports.ServiceUnavailableError = exports.UnauthorizedError = exports.PaymentRequiredError = exports.JsonSeoApiError = exports.JsonSeoError = void 0;
|
|
4
|
+
exports.apiErrorFor = apiErrorFor;
|
|
5
|
+
/** Общий предок всех ошибок SDK. */
|
|
6
|
+
class JsonSeoError extends Error {
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
// Иначе instanceof ломается в сборках под ES5.
|
|
11
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.JsonSeoError = JsonSeoError;
|
|
15
|
+
/** Сервис ответил отказом. Тело сохраняется целиком. */
|
|
16
|
+
class JsonSeoApiError extends JsonSeoError {
|
|
17
|
+
constructor(message, status, body, payload, retryAfter = null) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.body = body;
|
|
21
|
+
this.payload = payload;
|
|
22
|
+
this.retryAfter = retryAfter;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.JsonSeoApiError = JsonSeoApiError;
|
|
26
|
+
/** 402: на счёте не хватает средств. */
|
|
27
|
+
class PaymentRequiredError extends JsonSeoApiError {
|
|
28
|
+
}
|
|
29
|
+
exports.PaymentRequiredError = PaymentRequiredError;
|
|
30
|
+
/** 403 или 401: ключ не передан или недействителен. */
|
|
31
|
+
class UnauthorizedError extends JsonSeoApiError {
|
|
32
|
+
}
|
|
33
|
+
exports.UnauthorizedError = UnauthorizedError;
|
|
34
|
+
/** 503: выдачу получить не вышло. Деньги не списаны, повтор обычно проходит. */
|
|
35
|
+
class ServiceUnavailableError extends JsonSeoApiError {
|
|
36
|
+
}
|
|
37
|
+
exports.ServiceUnavailableError = ServiceUnavailableError;
|
|
38
|
+
/** 422: параметры запроса не приняты. Деньги не списываются. */
|
|
39
|
+
class ValidationError extends JsonSeoApiError {
|
|
40
|
+
/** Ошибки по именам параметров: `{ text: ['Введите запрос'] }`. */
|
|
41
|
+
get errors() {
|
|
42
|
+
const raw = this.payload.errors;
|
|
43
|
+
if (raw === null || typeof raw !== 'object') {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
const errors = {};
|
|
47
|
+
for (const [field, messages] of Object.entries(raw)) {
|
|
48
|
+
errors[field] = Array.isArray(messages) ? messages.map(String) : [String(messages)];
|
|
49
|
+
}
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
|
52
|
+
/** Забракованные параметры. */
|
|
53
|
+
get fields() {
|
|
54
|
+
return Object.keys(this.errors);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.ValidationError = ValidationError;
|
|
58
|
+
/** 429: превышен лимит частоты. Срок повтора — в `retryAfter`. */
|
|
59
|
+
class RateLimitError extends JsonSeoApiError {
|
|
60
|
+
}
|
|
61
|
+
exports.RateLimitError = RateLimitError;
|
|
62
|
+
/** До сервиса не достучались: сеть, DNS, TLS. Статуса нет. */
|
|
63
|
+
class NetworkError extends JsonSeoError {
|
|
64
|
+
constructor(message, cause) {
|
|
65
|
+
super(message, { cause });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.NetworkError = NetworkError;
|
|
69
|
+
/** Запрос не уложился в отведённое время и был прерван на стороне клиента. */
|
|
70
|
+
class TimeoutError extends NetworkError {
|
|
71
|
+
}
|
|
72
|
+
exports.TimeoutError = TimeoutError;
|
|
73
|
+
/**
|
|
74
|
+
* Заголовки пришли, а тело дочитать не вышло. Автоматически не повторяется:
|
|
75
|
+
* выдача уже собрана и оплачена, обрыв случился на отдаче.
|
|
76
|
+
*/
|
|
77
|
+
class IncompleteResponseError extends NetworkError {
|
|
78
|
+
}
|
|
79
|
+
exports.IncompleteResponseError = IncompleteResponseError;
|
|
80
|
+
/** Запрос прерван переданным в него AbortSignal. */
|
|
81
|
+
class AbortError extends JsonSeoError {
|
|
82
|
+
}
|
|
83
|
+
exports.AbortError = AbortError;
|
|
84
|
+
/** Запрос не отправлен: SDK забраковал аргументы ещё до обращения к сети. */
|
|
85
|
+
class InvalidArgumentError extends JsonSeoError {
|
|
86
|
+
}
|
|
87
|
+
exports.InvalidArgumentError = InvalidArgumentError;
|
|
88
|
+
/**
|
|
89
|
+
* Успех, но тело не разобралось как JSON. Тело сохраняется: страница уже
|
|
90
|
+
* оплачена, и достать из неё данные руками лучше, чем не иметь ничего.
|
|
91
|
+
*/
|
|
92
|
+
class ParseError extends JsonSeoError {
|
|
93
|
+
constructor(message, body) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.body = body;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.ParseError = ParseError;
|
|
99
|
+
/**
|
|
100
|
+
* Собирает ошибку под этот HTTP-статус.
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
function apiErrorFor(status, body, payload, retryAfter) {
|
|
105
|
+
const message = typeof payload.message === 'string' && payload.message !== ''
|
|
106
|
+
? payload.message
|
|
107
|
+
: `JSON SEO API вернул ошибку ${status}.`;
|
|
108
|
+
switch (status) {
|
|
109
|
+
case 401:
|
|
110
|
+
case 403:
|
|
111
|
+
return new UnauthorizedError(message, status, body, payload, retryAfter);
|
|
112
|
+
case 402:
|
|
113
|
+
return new PaymentRequiredError(message, status, body, payload, retryAfter);
|
|
114
|
+
case 422:
|
|
115
|
+
return new ValidationError(message, status, body, payload, retryAfter);
|
|
116
|
+
case 429:
|
|
117
|
+
return new RateLimitError(message, status, body, payload, retryAfter);
|
|
118
|
+
case 503:
|
|
119
|
+
return new ServiceUnavailableError(message, status, body, payload, retryAfter);
|
|
120
|
+
default:
|
|
121
|
+
return new JsonSeoApiError(message, status, body, payload, retryAfter);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ParamValue } from './common.js';
|
|
2
|
+
/** Глобальный `fetch` или любая его замена. */
|
|
3
|
+
export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
|
|
4
|
+
/** Куда класть ключ: в заголовок Authorization или в параметр key. */
|
|
5
|
+
export type AuthMode = 'header' | 'query';
|
|
6
|
+
/** Настройки клиента. */
|
|
7
|
+
export interface ClientOptions {
|
|
8
|
+
/** Ключ из личного кабинета на jsonseo.ru. */
|
|
9
|
+
apiKey: string;
|
|
10
|
+
/** Адрес API. По умолчанию https://jsonseo.ru/api */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Сколько ждать ответа на одну попытку. По умолчанию 300 000 мс. */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
/** Сколько всего попыток у запроса, включая первую. По умолчанию 3. */
|
|
15
|
+
attempts?: number;
|
|
16
|
+
/** Стартовая пауза между попытками, миллисекунд. По умолчанию 1000. */
|
|
17
|
+
retryDelayMs?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Потолок паузы между попытками, миллисекунд. По умолчанию 30 000. Если
|
|
20
|
+
* сервис просит ждать дольше, повторов не будет вовсе.
|
|
21
|
+
*/
|
|
22
|
+
maxRetryDelayMs?: number;
|
|
23
|
+
/** Где передавать ключ. В заголовке — чтобы не оседал в логах прокси. */
|
|
24
|
+
auth?: AuthMode;
|
|
25
|
+
/** Своя подпись клиента. */
|
|
26
|
+
userAgent?: string;
|
|
27
|
+
/** Своя реализация fetch — для тестов или прокси. */
|
|
28
|
+
fetch?: FetchLike;
|
|
29
|
+
}
|
|
30
|
+
/** Настройки одного запроса. */
|
|
31
|
+
export interface RequestOptions {
|
|
32
|
+
/** Отмена запроса снаружи. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** Таймаут именно этого запроса, миллисекунд. */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Транспорт: собирает запрос, разбирает ответ и решает, повторять ли отказ.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export declare class HttpClient {
|
|
43
|
+
private readonly apiKey;
|
|
44
|
+
private readonly baseUrl;
|
|
45
|
+
private readonly timeoutMs;
|
|
46
|
+
private readonly attempts;
|
|
47
|
+
private readonly retryDelayMs;
|
|
48
|
+
private readonly maxRetryDelayMs;
|
|
49
|
+
private readonly auth;
|
|
50
|
+
private readonly userAgent;
|
|
51
|
+
private readonly fetchImpl;
|
|
52
|
+
constructor(options: ClientOptions);
|
|
53
|
+
/** Запрос, ответ которого разбирается как JSON. */
|
|
54
|
+
json<T>(path: string, params: Record<string, ParamValue>, options?: RequestOptions): Promise<T>;
|
|
55
|
+
/** Запрос, ответ которого возвращается строкой без разбора. */
|
|
56
|
+
text(path: string, params: Record<string, ParamValue>, options?: RequestOptions): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Выполняет запрос, повторяя те отказы, за которые сервис не берёт денег:
|
|
59
|
+
* 429, 5xx и обрывы связи до того, как ответ начал приходить.
|
|
60
|
+
*/
|
|
61
|
+
private send;
|
|
62
|
+
/**
|
|
63
|
+
* Один заход в сеть: таймаут и внешняя отмена сводятся в один сигнал.
|
|
64
|
+
* Тело читается здесь же — fetch отдаёт ответ сразу по заголовкам, и
|
|
65
|
+
* снаружи застрявшая передача висела бы без ограничения по времени.
|
|
66
|
+
*/
|
|
67
|
+
private fetchOnce;
|
|
68
|
+
/** Попытки нумеруются с нуля: при attempts = 3 последняя — вторая. */
|
|
69
|
+
private isLastAttempt;
|
|
70
|
+
/**
|
|
71
|
+
* Пауза удваивается с каждой попыткой; случайная добавка разводит
|
|
72
|
+
* параллельные запросы, чтобы они не вернулись разом.
|
|
73
|
+
*/
|
|
74
|
+
private backoff;
|
|
75
|
+
}
|
|
76
|
+
/** Приводит параметры к тому виду, в каком их ждёт форма запроса. */
|
|
77
|
+
export declare function encodeParams(params: Record<string, ParamValue>): URLSearchParams;
|