@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 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
- | `url` | string \| URL \| Request | **обязательно** | Адрес запроса |
578
- | `options` | RequestInit | `undefined` | Параметры fetch (метод, заголовки, body и т.д.) |
579
- | `retries` | number | `5` | Количество попыток |
580
- | `delay` | number | `500` | Задержка между попытками (мс) |
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: string = process.env.CONFIG_DIR || "../config";
22
- const APP_ENV: string = process.env.APP_ENV || "PROD";
23
- const CLIENT_ID: string | undefined = APP_ENV === "DEV" ? process.env.APP_B24_CLIENT_ID_DEV : process.env.APP_B24_CLIENT_ID;
24
- const CLIENT_SECRET: string | undefined = APP_ENV === "DEV" ? process.env.APP_B24_CLIENT_SECRET_DEV : process.env.APP_B24_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
- /** Интервал проактивного обновления токена (мс). 25 минут при TTL токена 30 минут */
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
- // --- Создание экземпляра B24OAuth ---
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
- // --- Мьютекс для refresh токена ---
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
- export const $b24 = createB24Instance();
277
+ const _b24Raw = createB24Instance();
278
+ export const $b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
194
279
 
195
- if ($b24) {
196
- // Сохраняем токены в файл при каждом refresh (реактивном или проактивном)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrey4emk/npm-app-back-b24",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "Bitrix24 OAuth helpers for Node.js projects",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -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) {