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.
@@ -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,106 @@
1
+ /** Общий предок всех ошибок SDK. */
2
+ export class JsonSeoError extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = new.target.name;
6
+ // Иначе instanceof ломается в сборках под ES5.
7
+ Object.setPrototypeOf(this, new.target.prototype);
8
+ }
9
+ }
10
+ /** Сервис ответил отказом. Тело сохраняется целиком. */
11
+ export class JsonSeoApiError extends JsonSeoError {
12
+ constructor(message, status, body, payload, retryAfter = null) {
13
+ super(message);
14
+ this.status = status;
15
+ this.body = body;
16
+ this.payload = payload;
17
+ this.retryAfter = retryAfter;
18
+ }
19
+ }
20
+ /** 402: на счёте не хватает средств. */
21
+ export class PaymentRequiredError extends JsonSeoApiError {
22
+ }
23
+ /** 403 или 401: ключ не передан или недействителен. */
24
+ export class UnauthorizedError extends JsonSeoApiError {
25
+ }
26
+ /** 503: выдачу получить не вышло. Деньги не списаны, повтор обычно проходит. */
27
+ export class ServiceUnavailableError extends JsonSeoApiError {
28
+ }
29
+ /** 422: параметры запроса не приняты. Деньги не списываются. */
30
+ export class ValidationError extends JsonSeoApiError {
31
+ /** Ошибки по именам параметров: `{ text: ['Введите запрос'] }`. */
32
+ get errors() {
33
+ const raw = this.payload.errors;
34
+ if (raw === null || typeof raw !== 'object') {
35
+ return {};
36
+ }
37
+ const errors = {};
38
+ for (const [field, messages] of Object.entries(raw)) {
39
+ errors[field] = Array.isArray(messages) ? messages.map(String) : [String(messages)];
40
+ }
41
+ return errors;
42
+ }
43
+ /** Забракованные параметры. */
44
+ get fields() {
45
+ return Object.keys(this.errors);
46
+ }
47
+ }
48
+ /** 429: превышен лимит частоты. Срок повтора — в `retryAfter`. */
49
+ export class RateLimitError extends JsonSeoApiError {
50
+ }
51
+ /** До сервиса не достучались: сеть, DNS, TLS. Статуса нет. */
52
+ export class NetworkError extends JsonSeoError {
53
+ constructor(message, cause) {
54
+ super(message, { cause });
55
+ }
56
+ }
57
+ /** Запрос не уложился в отведённое время и был прерван на стороне клиента. */
58
+ export class TimeoutError extends NetworkError {
59
+ }
60
+ /**
61
+ * Заголовки пришли, а тело дочитать не вышло. Автоматически не повторяется:
62
+ * выдача уже собрана и оплачена, обрыв случился на отдаче.
63
+ */
64
+ export class IncompleteResponseError extends NetworkError {
65
+ }
66
+ /** Запрос прерван переданным в него AbortSignal. */
67
+ export class AbortError extends JsonSeoError {
68
+ }
69
+ /** Запрос не отправлен: SDK забраковал аргументы ещё до обращения к сети. */
70
+ export class InvalidArgumentError extends JsonSeoError {
71
+ }
72
+ /**
73
+ * Успех, но тело не разобралось как JSON. Тело сохраняется: страница уже
74
+ * оплачена, и достать из неё данные руками лучше, чем не иметь ничего.
75
+ */
76
+ export class ParseError extends JsonSeoError {
77
+ constructor(message, body) {
78
+ super(message);
79
+ this.body = body;
80
+ }
81
+ }
82
+ /**
83
+ * Собирает ошибку под этот HTTP-статус.
84
+ *
85
+ * @internal
86
+ */
87
+ export function apiErrorFor(status, body, payload, retryAfter) {
88
+ const message = typeof payload.message === 'string' && payload.message !== ''
89
+ ? payload.message
90
+ : `JSON SEO API вернул ошибку ${status}.`;
91
+ switch (status) {
92
+ case 401:
93
+ case 403:
94
+ return new UnauthorizedError(message, status, body, payload, retryAfter);
95
+ case 402:
96
+ return new PaymentRequiredError(message, status, body, payload, retryAfter);
97
+ case 422:
98
+ return new ValidationError(message, status, body, payload, retryAfter);
99
+ case 429:
100
+ return new RateLimitError(message, status, body, payload, retryAfter);
101
+ case 503:
102
+ return new ServiceUnavailableError(message, status, body, payload, retryAfter);
103
+ default:
104
+ return new JsonSeoApiError(message, status, body, payload, retryAfter);
105
+ }
106
+ }
@@ -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;
@@ -0,0 +1,302 @@
1
+ import { AbortError, apiErrorFor, IncompleteResponseError, InvalidArgumentError, JsonSeoError, NetworkError, ParseError, TimeoutError, } from './errors.js';
2
+ const DEFAULT_BASE_URL = 'https://jsonseo.ru/api';
3
+ /**
4
+ * Ключи проверяются типом: забытая здесь настройка из ClientOptions
5
+ * отвергалась бы у пользователя как незнакомая.
6
+ */
7
+ const KNOWN_OPTIONS = {
8
+ apiKey: true,
9
+ baseUrl: true,
10
+ timeoutMs: true,
11
+ attempts: true,
12
+ retryDelayMs: true,
13
+ maxRetryDelayMs: true,
14
+ auth: true,
15
+ userAgent: true,
16
+ fetch: true,
17
+ };
18
+ const KNOWN_OPTION_NAMES = Object.keys(KNOWN_OPTIONS);
19
+ const VERSION = '1.0.0';
20
+ /**
21
+ * Транспорт: собирает запрос, разбирает ответ и решает, повторять ли отказ.
22
+ *
23
+ * @internal
24
+ */
25
+ export class HttpClient {
26
+ constructor(options) {
27
+ if (typeof options.apiKey !== 'string' || options.apiKey.trim() === '') {
28
+ throw new InvalidArgumentError('Нужен API-ключ: возьмите его в личном кабинете на https://jsonseo.ru.');
29
+ }
30
+ // undefined из спреда частичного конфига — не настройка, а её отсутствие.
31
+ const unknown = Object.keys(options).filter((name) => !KNOWN_OPTION_NAMES.includes(name) && options[name] !== undefined);
32
+ if (unknown.length > 0) {
33
+ throw new InvalidArgumentError(`Неизвестные настройки клиента: ${unknown.join(', ')}. Доступны: ${KNOWN_OPTION_NAMES.join(', ')}.`);
34
+ }
35
+ // NaN сюда приезжает из Number(process.env.ЧЕГО_НЕТ), и без проверки
36
+ // сравнение с ним всегда ложно — повторы платного запроса не кончались бы.
37
+ if (options.attempts !== undefined && (!Number.isInteger(options.attempts) || options.attempts < 1)) {
38
+ throw new InvalidArgumentError(`Настройка attempts ожидает целое число не меньше 1, получено: ${String(options.attempts)}.`);
39
+ }
40
+ if (options.auth !== undefined && options.auth !== 'header' && options.auth !== 'query') {
41
+ throw new InvalidArgumentError(`Настройка auth принимает "header" или "query", получено: ${String(options.auth)}.`);
42
+ }
43
+ const fetchImpl = options.fetch ?? (typeof fetch === 'function' ? fetch : undefined);
44
+ if (!fetchImpl) {
45
+ throw new InvalidArgumentError('В этой среде нет глобального fetch. Нужен Node 18+, Bun или Deno — либо передайте свою реализацию настройкой fetch.');
46
+ }
47
+ this.apiKey = options.apiKey.trim();
48
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
49
+ this.timeoutMs = options.timeoutMs ?? 300000;
50
+ this.attempts = options.attempts ?? 3;
51
+ this.retryDelayMs = options.retryDelayMs ?? 1000;
52
+ this.maxRetryDelayMs = options.maxRetryDelayMs ?? 30000;
53
+ this.auth = options.auth ?? 'header';
54
+ this.userAgent = options.userAgent ?? `jsonseo-node/${VERSION}`;
55
+ this.fetchImpl = fetchImpl;
56
+ }
57
+ /** Запрос, ответ которого разбирается как JSON. */
58
+ async json(path, params, options) {
59
+ const body = await this.send(path, params, 'application/json', options);
60
+ try {
61
+ return JSON.parse(body);
62
+ }
63
+ catch (error) {
64
+ // Тело кладём в ошибку: страница выдачи уже оплачена.
65
+ throw new ParseError(`Ответ JSON SEO API не разобрался как JSON: ${error.message}.`, body);
66
+ }
67
+ }
68
+ /** Запрос, ответ которого возвращается строкой без разбора. */
69
+ text(path, params, options) {
70
+ return this.send(path, params, 'application/xml, text/xml', options);
71
+ }
72
+ /**
73
+ * Выполняет запрос, повторяя те отказы, за которые сервис не берёт денег:
74
+ * 429, 5xx и обрывы связи до того, как ответ начал приходить.
75
+ */
76
+ async send(path, params, accept, options) {
77
+ const query = encodeParams(params);
78
+ if (this.auth === 'query') {
79
+ query.set('key', this.apiKey);
80
+ }
81
+ const headers = {
82
+ Accept: accept,
83
+ 'Content-Type': 'application/x-www-form-urlencoded',
84
+ 'User-Agent': this.userAgent,
85
+ };
86
+ if (this.auth === 'header') {
87
+ headers.Authorization = `Bearer ${this.apiKey}`;
88
+ }
89
+ // Всегда POST: длинные списки фраз в GET не помещаются.
90
+ const url = `${this.baseUrl}/${path.replace(/^\/+/, '')}`;
91
+ const body = query.toString();
92
+ for (let attempt = 0;; attempt++) {
93
+ let response;
94
+ try {
95
+ response = await this.fetchOnce(url, headers, body, options);
96
+ }
97
+ catch (error) {
98
+ // Таймаут и обрыв на середине тела не повторяем: выдача уже
99
+ // собрана и оплачена.
100
+ if (error instanceof NetworkError &&
101
+ !(error instanceof TimeoutError) &&
102
+ !(error instanceof IncompleteResponseError) &&
103
+ !this.isLastAttempt(attempt)) {
104
+ try {
105
+ await sleep(this.backoff(attempt), options?.signal);
106
+ }
107
+ catch (aborted) {
108
+ // Отменили во время паузы: причину ожидания не теряем.
109
+ aborted.cause = error;
110
+ throw aborted;
111
+ }
112
+ continue;
113
+ }
114
+ throw error;
115
+ }
116
+ if (response.ok) {
117
+ return response.body;
118
+ }
119
+ const { retryAfter } = response;
120
+ const error = apiErrorFor(response.status, response.body, parseQuietly(response.body), retryAfter);
121
+ // Проснуться раньше названного срока — снова получить тот же отказ.
122
+ // Ждать дольше потолка не станем: отдаём ошибку.
123
+ if (this.isLastAttempt(attempt) ||
124
+ !isRetryable(response.status) ||
125
+ (retryAfter !== null && retryAfter * 1000 > this.maxRetryDelayMs)) {
126
+ throw error;
127
+ }
128
+ // Не раньше, чем просит сервис, и не чаще своего бэкоффа:
129
+ // Retry-After прошедшей датой даёт ноль.
130
+ const pause = retryAfter === null ? this.backoff(attempt) : Math.max(retryAfter * 1000, this.backoff(attempt));
131
+ await sleep(pause, options?.signal);
132
+ }
133
+ }
134
+ /**
135
+ * Один заход в сеть: таймаут и внешняя отмена сводятся в один сигнал.
136
+ * Тело читается здесь же — fetch отдаёт ответ сразу по заголовкам, и
137
+ * снаружи застрявшая передача висела бы без ограничения по времени.
138
+ */
139
+ async fetchOnce(url, headers, body, options) {
140
+ const external = options?.signal;
141
+ if (external?.aborted) {
142
+ throw new AbortError('Запрос отменён до отправки.');
143
+ }
144
+ const controller = new AbortController();
145
+ const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
146
+ let timedOut = false;
147
+ const timer = setTimeout(() => {
148
+ timedOut = true;
149
+ controller.abort();
150
+ }, timeoutMs);
151
+ const forward = () => controller.abort();
152
+ external?.addEventListener('abort', forward);
153
+ try {
154
+ const response = await this.fetchImpl(url, {
155
+ method: 'POST',
156
+ headers,
157
+ body,
158
+ signal: controller.signal,
159
+ redirect: 'follow',
160
+ });
161
+ let text;
162
+ try {
163
+ text = await response.text();
164
+ }
165
+ catch (error) {
166
+ if (timedOut) {
167
+ throw new TimeoutError(`Ответа от JSON SEO API не дождались за ${timeoutMs} мс.`, error);
168
+ }
169
+ if (external?.aborted) {
170
+ throw new AbortError('Запрос отменён.');
171
+ }
172
+ // Тело дочитать не вышло, а выдача уже оплачена — отдельный
173
+ // класс ошибки, повторять такое нельзя.
174
+ throw new IncompleteResponseError(`Ответ от JSON SEO API пришёл не целиком: ${error.message}.`, error);
175
+ }
176
+ return {
177
+ status: response.status,
178
+ ok: response.ok,
179
+ retryAfter: parseRetryAfter(response.headers.get('retry-after')),
180
+ body: text,
181
+ };
182
+ }
183
+ catch (error) {
184
+ if (error instanceof JsonSeoError) {
185
+ throw error;
186
+ }
187
+ if (timedOut) {
188
+ throw new TimeoutError(`Ответа от JSON SEO API не дождались за ${timeoutMs} мс.`, error);
189
+ }
190
+ if (external?.aborted) {
191
+ throw new AbortError('Запрос отменён.');
192
+ }
193
+ throw new NetworkError(`Запрос к JSON SEO API не удался: ${error.message}.`, error);
194
+ }
195
+ finally {
196
+ clearTimeout(timer);
197
+ external?.removeEventListener('abort', forward);
198
+ }
199
+ }
200
+ /** Попытки нумеруются с нуля: при attempts = 3 последняя — вторая. */
201
+ isLastAttempt(attempt) {
202
+ return attempt + 1 >= this.attempts;
203
+ }
204
+ /**
205
+ * Пауза удваивается с каждой попыткой; случайная добавка разводит
206
+ * параллельные запросы, чтобы они не вернулись разом.
207
+ */
208
+ backoff(attempt) {
209
+ const delay = this.retryDelayMs * 2 ** attempt;
210
+ // Потолок накладывается после добавки, иначе она бы его превышала.
211
+ return Math.min(delay + delay * 0.25 * Math.random(), this.maxRetryDelayMs);
212
+ }
213
+ }
214
+ /** Приводит параметры к тому виду, в каком их ждёт форма запроса. */
215
+ export function encodeParams(params) {
216
+ const query = new URLSearchParams();
217
+ for (const [name, value] of Object.entries(params)) {
218
+ if (value === null || value === undefined) {
219
+ continue;
220
+ }
221
+ if (Array.isArray(value)) {
222
+ // Пустой список — «параметр не задан»: от region= сервис откажет.
223
+ if (value.length === 0) {
224
+ continue;
225
+ }
226
+ // Фразы — переводом строки: запятая в них встречается.
227
+ query.set(name, value.map((item, index) => scalar(`${name}[${index}]`, item)).join(name === 'phrases' ? '\n' : ','));
228
+ continue;
229
+ }
230
+ query.set(name, scalar(name, value));
231
+ }
232
+ return query;
233
+ }
234
+ function scalar(name, value) {
235
+ if (typeof value === 'boolean') {
236
+ return value ? '1' : '0';
237
+ }
238
+ if (typeof value === 'number') {
239
+ if (!Number.isFinite(value)) {
240
+ throw new InvalidArgumentError(`Параметр ${name} получил не число: ${String(value)}.`);
241
+ }
242
+ return String(value);
243
+ }
244
+ if (typeof value === 'string') {
245
+ return value;
246
+ }
247
+ throw new InvalidArgumentError(`Параметр ${name} должен быть строкой, числом, флагом или массивом таких значений.`);
248
+ }
249
+ function isRetryable(status) {
250
+ return status === 429 || status >= 500;
251
+ }
252
+ /**
253
+ * Сколько секунд просит подождать сервис. RFC 9110 разрешает число секунд
254
+ * и HTTP-дату, разбираются обе.
255
+ */
256
+ function parseRetryAfter(header) {
257
+ if (header === null) {
258
+ return null;
259
+ }
260
+ const value = header.trim();
261
+ if (/^\d+$/.test(value)) {
262
+ return Number(value);
263
+ }
264
+ const timestamp = Date.parse(value);
265
+ if (Number.isNaN(timestamp)) {
266
+ return null;
267
+ }
268
+ return Math.max(0, Math.round((timestamp - Date.now()) / 1000));
269
+ }
270
+ /** Тело ошибки может быть и не JSON — тогда подробностей просто нет. */
271
+ function parseQuietly(body) {
272
+ try {
273
+ const parsed = JSON.parse(body);
274
+ return parsed !== null && typeof parsed === 'object' ? parsed : {};
275
+ }
276
+ catch {
277
+ return {};
278
+ }
279
+ }
280
+ /**
281
+ * Прерываемая пауза: без учёта сигнала отмена замечалась бы только через
282
+ * всю паузу целиком, до 30 секунд при значениях по умолчанию.
283
+ */
284
+ function sleep(ms, signal) {
285
+ if (!signal) {
286
+ return new Promise((resolve) => setTimeout(resolve, ms));
287
+ }
288
+ if (signal.aborted) {
289
+ return Promise.reject(new AbortError('Запрос отменён.'));
290
+ }
291
+ return new Promise((resolve, reject) => {
292
+ const timer = setTimeout(() => {
293
+ signal.removeEventListener('abort', onAbort);
294
+ resolve();
295
+ }, ms);
296
+ function onAbort() {
297
+ clearTimeout(timer);
298
+ reject(new AbortError('Запрос отменён.'));
299
+ }
300
+ signal.addEventListener('abort', onAbort, { once: true });
301
+ });
302
+ }
@@ -0,0 +1,9 @@
1
+ export { JsonSeoClient } from './client.js';
2
+ export { encodeParams } from './http.js';
3
+ export type { AuthMode, ClientOptions, FetchLike, RequestOptions } from './http.js';
4
+ export { AbortError, IncompleteResponseError, InvalidArgumentError, JsonSeoApiError, JsonSeoError, NetworkError, ParseError, PaymentRequiredError, RateLimitError, ServiceUnavailableError, TimeoutError, UnauthorizedError, ValidationError, } from './errors.js';
5
+ export type * from './common.js';
6
+ export type * from './params.js';
7
+ export type * from './responses.js';
8
+ import { JsonSeoClient } from './client.js';
9
+ export default JsonSeoClient;
@@ -0,0 +1,5 @@
1
+ export { JsonSeoClient } from './client.js';
2
+ export { encodeParams } from './http.js';
3
+ export { AbortError, IncompleteResponseError, InvalidArgumentError, JsonSeoApiError, JsonSeoError, NetworkError, ParseError, PaymentRequiredError, RateLimitError, ServiceUnavailableError, TimeoutError, UnauthorizedError, ValidationError, } from './errors.js';
4
+ import { JsonSeoClient } from './client.js';
5
+ export default JsonSeoClient;
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }