@andrey4emk/npm-app-back-b24 2.0.9 → 2.0.10
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/README.md +33 -7
- package/bitrix24/b24.ts +103 -22
- package/package.json +1 -1
- package/utils/fetchRetry.ts +3 -2
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
Wappi,
|
|
43
43
|
logs,
|
|
44
44
|
fetchRetry,
|
|
45
|
+
isNetworkError,
|
|
45
46
|
} from "@andrey4emk/npm-app-back-b24";
|
|
46
47
|
|
|
47
48
|
// $b24 — готовый экземпляр B24OAuth (или null, если токены не настроены)
|
|
@@ -113,6 +114,17 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
113
114
|
- При обновлении токенов SDK автоматически сохраняет их в файл через callback `setCallbackRefreshAuth`.
|
|
114
115
|
- Окружение (`DEV`/`PROD`) определяется переменной `APP_ENV` — от неё зависят используемые `CLIENT_ID`/`CLIENT_SECRET`.
|
|
115
116
|
|
|
117
|
+
**Retry при сетевых ошибках:**
|
|
118
|
+
|
|
119
|
+
Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch` и `fetchListMethod`.
|
|
120
|
+
|
|
121
|
+
| Параметр | Значение |
|
|
122
|
+
| -------------- | -------- |
|
|
123
|
+
| Попыток | 5 |
|
|
124
|
+
| Задержка | 500 мс |
|
|
125
|
+
|
|
126
|
+
Retry срабатывает только при сетевых проблемах (`ECONNRESET`, `ETIMEDOUT`, `ERR_NETWORK` и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 **не** вызывают повторных попыток. Каждая неудачная попытка логируется через `logs.add()` с уровнем `error`.
|
|
127
|
+
|
|
116
128
|
- **`saveAuthB24Handler(req, res)`** — HTTP-обработчик для сохранения набора токенов из `req.body`.
|
|
117
129
|
|
|
118
130
|
- **Параметры:**
|
|
@@ -570,14 +582,28 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
570
582
|
|
|
571
583
|
- **`fetchRetry(url, options?, retries?, delay?)`** — обёртка над `fetch` с повторными попытками при сетевых ошибках. HTTP-ошибки (4xx, 5xx) **не** вызывают повторных попыток — повторяются только сетевые сбои (TypeError, ECONNRESET, ETIMEDOUT и т.д.).
|
|
572
584
|
|
|
573
|
-
|
|
585
|
+
- **`isNetworkError(error)`** — проверяет, является ли ошибка сетевой (стоит повторить запрос). Используется внутри `fetchRetry` и retry-обёртки `$b24`. Можно использовать в своём коде для аналогичных проверок.
|
|
586
|
+
|
|
587
|
+
```js
|
|
588
|
+
import { isNetworkError } from "@andrey4emk/npm-app-back-b24";
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
await someRequest();
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (isNetworkError(error)) {
|
|
594
|
+
// сетевая ошибка — можно повторить
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Параметры:**
|
|
574
600
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
601
|
+
| Параметр | Тип | По умолчанию | Описание |
|
|
602
|
+
| --------- | ---------------------------- | --------------- | ----------------------------------------------- |
|
|
603
|
+
| `url` | string \| URL \| Request | **обязательно** | Адрес запроса |
|
|
604
|
+
| `options` | RequestInit | `undefined` | Параметры fetch (метод, заголовки, body и т.д.) |
|
|
605
|
+
| `retries` | number | `5` | Количество попыток |
|
|
606
|
+
| `delay` | number | `500` | Задержка между попытками (мс) |
|
|
581
607
|
|
|
582
608
|
- **Возвращает:** `Promise<Response>` — ответ от fetch.
|
|
583
609
|
|
package/bitrix24/b24.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { B24OAuth } from "@bitrix24/b24jssdk";
|
|
1
|
+
import { B24OAuth, AjaxError } from "@bitrix24/b24jssdk";
|
|
2
2
|
import type { B24OAuthParams, B24OAuthSecret, AuthData } from "@bitrix24/b24jssdk";
|
|
3
3
|
import { logs } from "../logs/logs.ts";
|
|
4
|
+
import { isNetworkError } from "../utils/fetchRetry.ts";
|
|
4
5
|
import Conf from "conf";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import type { Request, Response } from "express";
|
|
@@ -8,7 +9,7 @@ import type { Request, Response } from "express";
|
|
|
8
9
|
import dotEnv from "dotenv";
|
|
9
10
|
dotEnv.config();
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
+
// ==================== Типы ====================
|
|
12
13
|
|
|
13
14
|
/** Результат операций с токенами */
|
|
14
15
|
interface SaveResult {
|
|
@@ -16,29 +17,43 @@ interface SaveResult {
|
|
|
16
17
|
message: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
+
// ==================== Константы ====================
|
|
20
21
|
|
|
21
|
-
const CONFIG_DIR
|
|
22
|
-
const APP_ENV
|
|
23
|
-
const CLIENT_ID
|
|
24
|
-
const CLIENT_SECRET
|
|
22
|
+
const CONFIG_DIR = process.env.CONFIG_DIR || "../config";
|
|
23
|
+
const APP_ENV = process.env.APP_ENV || "PROD";
|
|
24
|
+
const CLIENT_ID = APP_ENV === "DEV" ? process.env.APP_B24_CLIENT_ID_DEV : process.env.APP_B24_CLIENT_ID;
|
|
25
|
+
const CLIENT_SECRET = APP_ENV === "DEV" ? process.env.APP_B24_CLIENT_SECRET_DEV : process.env.APP_B24_CLIENT_SECRET;
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
+
/** 25 минут при TTL токена 30 минут */
|
|
27
28
|
const PROACTIVE_REFRESH_INTERVAL_MS = 25 * 60 * 1000;
|
|
28
29
|
|
|
30
|
+
/** Количество попыток при сетевых ошибках */
|
|
31
|
+
const RETRY_COUNT = 5;
|
|
32
|
+
/** Задержка между попытками (мс) */
|
|
33
|
+
const RETRY_DELAY_MS = 500;
|
|
34
|
+
|
|
35
|
+
/** Методы B24OAuth, оборачиваемые retry-логикой */
|
|
36
|
+
const RETRYABLE_METHODS = new Set(["callMethod", "callListMethod", "callBatch", "fetchListMethod"]);
|
|
37
|
+
|
|
38
|
+
/** Коды AxiosError, означающие проблему с сетью */
|
|
39
|
+
const AXIOS_NETWORK_CODES = new Set(["ERR_NETWORK", "ECONNABORTED"]);
|
|
40
|
+
|
|
29
41
|
const confAuthB24 = new Conf({
|
|
30
42
|
cwd: path.resolve(CONFIG_DIR),
|
|
31
43
|
configName: "authB24",
|
|
32
44
|
});
|
|
33
45
|
|
|
34
|
-
//
|
|
46
|
+
// ==================== Утилиты ====================
|
|
35
47
|
|
|
36
48
|
/** Убирает протокол из домена (https://example.bitrix24.ru → example.bitrix24.ru) */
|
|
37
49
|
function cleanDomain(domain: string): string {
|
|
38
50
|
return domain.replace(/^https?:\/\//, "");
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
/** Задержка на указанное количество миллисекунд */
|
|
54
|
+
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
|
|
56
|
+
// ==================== Создание экземпляра B24OAuth ====================
|
|
42
57
|
|
|
43
58
|
function createB24Instance(): B24OAuth | null {
|
|
44
59
|
const store = confAuthB24.store as Record<string, AuthData>;
|
|
@@ -77,7 +92,7 @@ function createB24Instance(): B24OAuth | null {
|
|
|
77
92
|
return new B24OAuth(authParams, secret);
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
//
|
|
95
|
+
// ==================== Работа с токенами ====================
|
|
81
96
|
|
|
82
97
|
/** Сохраняет токены в authB24.json, нормализуя домен без протокола */
|
|
83
98
|
export function saveTokens(authData: AuthData): SaveResult {
|
|
@@ -116,7 +131,7 @@ export function saveAuthB24Handler(req: Request, res: Response): void {
|
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
//
|
|
134
|
+
// ==================== Мьютекс для refresh токена ====================
|
|
120
135
|
|
|
121
136
|
/**
|
|
122
137
|
* Дедупликация refresh-запросов.
|
|
@@ -161,7 +176,7 @@ export async function refreshAndSaveTokens(): Promise<SaveResult> {
|
|
|
161
176
|
}
|
|
162
177
|
}
|
|
163
178
|
|
|
164
|
-
//
|
|
179
|
+
// ==================== Проактивное обновление токена ====================
|
|
165
180
|
|
|
166
181
|
let proactiveRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
167
182
|
|
|
@@ -188,13 +203,82 @@ export function stopProactiveRefresh(): void {
|
|
|
188
203
|
logs.add("Проактивное обновление токена остановлено", "debug");
|
|
189
204
|
}
|
|
190
205
|
|
|
191
|
-
//
|
|
206
|
+
// ==================== Retry-обёртка ====================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Проверяет, является ли ошибка из SDK сетевой.
|
|
210
|
+
* AjaxError хранит AxiosError в поле originalError.
|
|
211
|
+
*/
|
|
212
|
+
function isB24NetworkError(error: unknown): boolean {
|
|
213
|
+
if (error instanceof AjaxError) {
|
|
214
|
+
const { originalError, status } = error;
|
|
215
|
+
|
|
216
|
+
if (originalError && isNetworkError(originalError)) return true;
|
|
217
|
+
if (AXIOS_NETWORK_CODES.has((originalError as any)?.code)) return true;
|
|
218
|
+
if (status === 0) return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return isNetworkError(error);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Оборачивает async-функцию retry-логикой при сетевых ошибках */
|
|
225
|
+
function withRetry<T extends (...args: any[]) => Promise<any>>(fn: T, context: any, methodName: string): T {
|
|
226
|
+
return (async (...args: any[]) => {
|
|
227
|
+
let lastError: unknown;
|
|
228
|
+
|
|
229
|
+
for (let attempt = 1; attempt <= RETRY_COUNT; attempt++) {
|
|
230
|
+
try {
|
|
231
|
+
return await fn.apply(context, args);
|
|
232
|
+
} catch (error: unknown) {
|
|
233
|
+
lastError = error;
|
|
234
|
+
|
|
235
|
+
if (!isB24NetworkError(error) || attempt === RETRY_COUNT) {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
240
|
+
logs.add(`$b24.${methodName}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${RETRY_DELAY_MS}мс`, "error");
|
|
241
|
+
|
|
242
|
+
await delay(RETRY_DELAY_MS);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Недостижимо при RETRY_COUNT >= 1, нужно для TypeScript
|
|
247
|
+
throw lastError;
|
|
248
|
+
}) as T;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Proxy-обёртка вокруг B24OAuth: автоматически оборачивает
|
|
253
|
+
* callMethod, callListMethod, callBatch и fetchListMethod retry-логикой.
|
|
254
|
+
* Обёрнутые методы кешируются — обёртка создаётся один раз на метод.
|
|
255
|
+
*/
|
|
256
|
+
function wrapB24WithRetry(b24: B24OAuth): B24OAuth {
|
|
257
|
+
const methodCache = new Map<string, Function>();
|
|
258
|
+
|
|
259
|
+
return new Proxy(b24, {
|
|
260
|
+
get(target, prop, receiver) {
|
|
261
|
+
const value = Reflect.get(target, prop, receiver);
|
|
262
|
+
|
|
263
|
+
if (typeof prop === "string" && RETRYABLE_METHODS.has(prop) && typeof value === "function") {
|
|
264
|
+
if (!methodCache.has(prop)) {
|
|
265
|
+
methodCache.set(prop, withRetry(value, target, prop));
|
|
266
|
+
}
|
|
267
|
+
return methodCache.get(prop);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return value;
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ==================== Инициализация ====================
|
|
192
276
|
|
|
193
|
-
|
|
277
|
+
const _b24Raw = createB24Instance();
|
|
278
|
+
export const $b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
|
|
194
279
|
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
$b24.setCallbackRefreshAuth(async ({ authData }) => {
|
|
280
|
+
if (_b24Raw) {
|
|
281
|
+
_b24Raw.setCallbackRefreshAuth(async ({ authData }) => {
|
|
198
282
|
const result = saveTokens(authData);
|
|
199
283
|
if (result.error) {
|
|
200
284
|
logs.add(`Ошибка при автосохранении токенов: ${result.message}`, "error");
|
|
@@ -203,8 +287,5 @@ if ($b24) {
|
|
|
203
287
|
}
|
|
204
288
|
});
|
|
205
289
|
|
|
206
|
-
|
|
207
|
-
refreshAndSaveTokens().then(() => {
|
|
208
|
-
startProactiveRefresh();
|
|
209
|
-
});
|
|
290
|
+
refreshAndSaveTokens().then(() => startProactiveRefresh());
|
|
210
291
|
}
|
package/package.json
CHANGED
package/utils/fetchRetry.ts
CHANGED
|
@@ -13,9 +13,10 @@ const NETWORK_ERROR_CODES = [
|
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Проверяет, является ли ошибка сетевой (стоит повторить запрос)
|
|
16
|
+
* Проверяет, является ли ошибка сетевой (стоит повторить запрос).
|
|
17
|
+
* Экспортируется для использования в других retry-обёртках (например, для $b24).
|
|
17
18
|
*/
|
|
18
|
-
function isNetworkError(error: unknown): boolean {
|
|
19
|
+
export function isNetworkError(error: unknown): boolean {
|
|
19
20
|
if (error instanceof TypeError) return true;
|
|
20
21
|
|
|
21
22
|
if (error instanceof Error) {
|