@andrey4emk/npm-app-back-b24 3.3.0 → 3.4.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/README.md CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  refreshAndSaveTokens,
36
36
  reinitializeB24,
37
37
  stopProactiveRefresh,
38
+ getResultData,
38
39
  errorB24,
39
40
  event,
40
41
  Event,
@@ -118,14 +119,20 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
118
119
 
119
120
  **Retry при сетевых ошибках:**
120
121
 
121
- Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch` и `fetchListMethod`, а также к обновлению токена (`refreshAuth`).
122
+ Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch`, а также к обновлению токена (`refreshAuth`). Метод `fetchListMethod` из ретраев исключён — это async-генератор, оборачивать его в async-функцию нельзя; постраничные запросы SDK ретраит сам.
122
123
 
123
124
  | Параметр | Значение |
124
125
  | -------------- | ----------------------------------------- |
125
126
  | Попыток | 5 |
126
- | Задержка | 500 мс (экспоненциально для refreshAuth) |
127
+ | Задержка | 500 мс (линейно растущая для refreshAuth) |
127
128
 
128
- Retry срабатывает только при сетевых проблемах (`ECONNRESET`, `ETIMEDOUT`, `ERR_NETWORK` и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 **не** вызывают повторных попыток. Каждая неудачная попытка логируется через `logs.add()` с уровнем `error`.
129
+ Retry срабатывает только при сетевых проблемах (`ECONNRESET`, `ETIMEDOUT`, `ERR_NETWORK` и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 **не** вызывают повторных попыток. Каждая неудачная попытка логируется через `logs.add()` с уровнем `error` — в сообщение попадает код ошибки SDK.
130
+
131
+ У самого SDK есть собственный слой ретраев (`maxRetries: 3`), но он работает только при rate limit (HTTP 429, 503) и при распознанных `NETWORK_ERROR` / `REQUEST_TIMEOUT`. Замаскированные транспортные сбои приходят с кодом `JSSDK_UNKNOWN_ERROR`, который входит в `BUILT_IN_HARD_ERROR_CODES` — на таких ошибках SDK делает одну попытку, и повторяет только Proxy.
132
+
133
+ **«Мягкие» ошибки Bitrix24:**
134
+
135
+ Часть ошибок (`ERROR_ENTITY_NOT_FOUND`, `BITRIX_REST_V3_EXCEPTION_*`) SDK не бросает исключением, а возвращает как `AjaxResult` с ошибкой: `isSuccess === false`, и тогда `response.getData()` вернёт `undefined`. Отдельный случай — успешный ответ, у которого сам `result` пустой (`null`/`undefined`). Чтобы не падать на `undefined`, разбирайте ответы через `getResultData()` — он покрывает обе ситуации и бросает понятную ошибку с именем метода.
129
136
 
130
137
  **Проактивное обновление токена:**
131
138
 
@@ -193,6 +200,20 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
193
200
  });
194
201
  ```
195
202
 
203
+ - **`getResultData(response, methodName)`** — достаёт `result` из ответа SDK. Если Bitrix24 вернул «мягкую» ошибку (`AjaxResult` с ошибкой вместо исключения) или пустой ответ, бросает `Error` с понятным текстом вместо падения на `undefined`.
204
+
205
+ - **Параметры:**
206
+ - `response` (AjaxResult) — результат `$b24.callMethod()`.
207
+ - `methodName` (string) — имя метода B24, попадёт в текст ошибки.
208
+ - **Возвращает:** содержимое `result`. Тип задаётся дженериком.
209
+
210
+ ```js
211
+ import { $b24, getResultData } from "@andrey4emk/npm-app-back-b24";
212
+
213
+ const response = await $b24.callMethod("crm.deal.get", { id: 123 });
214
+ const deal = getResultData(response, "crm.deal.get");
215
+ ```
216
+
196
217
  ### Задачи ошибок
197
218
 
198
219
  - **`errorB24(dataTask)`** — создаёт служебную задачу в Bitrix24 при ошибках/событиях. Использует глобальный `$b24`.
package/bitrix24/b24.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B24OAuth, AjaxError } from "@bitrix24/b24jssdk";
2
- import type { B24OAuthParams, B24OAuthSecret, AuthData } from "@bitrix24/b24jssdk";
1
+ import { B24OAuth, AjaxError, SdkError, Logger } from "@bitrix24/b24jssdk";
2
+ import type { B24OAuthParams, B24OAuthSecret, AuthData, AjaxResult } from "@bitrix24/b24jssdk";
3
3
  import { logs } from "../logs/logs.ts";
4
4
  import { isNetworkError } from "../utils/fetchRetry.ts";
5
5
  import Conf from "conf";
@@ -34,12 +34,15 @@ const RETRY_COUNT = 5;
34
34
  /** Задержка между попытками (мс) */
35
35
  const RETRY_DELAY_MS = 500;
36
36
 
37
- /** Методы B24OAuth, оборачиваемые retry-логикой */
38
- const RETRYABLE_METHODS = new Set(["callMethod", "callListMethod", "callBatch", "fetchListMethod"]);
37
+ /** Методы B24OAuth, оборачиваемые retry-логикой (fetchListMethod — async-генератор, его оборачивать нельзя) */
38
+ const RETRYABLE_METHODS = new Set(["callMethod", "callListMethod", "callBatch"]);
39
39
 
40
40
  /** Коды AxiosError, означающие проблему с сетью */
41
41
  const AXIOS_NETWORK_CODES = new Set(["ERR_NETWORK", "ECONNABORTED"]);
42
42
 
43
+ /** Коды ошибок SDK (AjaxError / RefreshTokenError), означающие транспортный сбой */
44
+ const SDK_NETWORK_CODES = new Set(["NETWORK_ERROR", "REQUEST_TIMEOUT", "ERR_NETWORK", "ECONNABORTED"]);
45
+
43
46
  const confAuthB24 = new Conf({
44
47
  cwd: path.resolve(CONFIG_DIR),
45
48
  configName: "authB24",
@@ -55,6 +58,31 @@ function cleanDomain(domain: string): string {
55
58
  /** Задержка на указанное количество миллисекунд */
56
59
  const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
57
60
 
61
+ /**
62
+ * Достаёт result из ответа SDK.
63
+ *
64
+ * SDK при «мягких» ошибках (ERROR_ENTITY_NOT_FOUND, BITRIX_REST_V3_EXCEPTION_*)
65
+ * не бросает исключение, а возвращает AjaxResult с ошибкой.
66
+ * Функция превращает такой ответ в понятную ошибку вместо падения на undefined.
67
+ * Также отсекает случай, когда запрос успешен, но result пустой (null/undefined).
68
+ *
69
+ * @param response — результат $b24.callMethod()
70
+ * @param methodName — имя метода B24, попадёт в текст ошибки
71
+ */
72
+ export function getResultData<T = any>(response: AjaxResult, methodName: string): T {
73
+ if (!response.isSuccess) {
74
+ const messages = response.getErrorMessages().join("; ") || "неизвестная ошибка";
75
+ throw new Error(`${methodName}: Bitrix24 вернул ошибку — ${messages}`);
76
+ }
77
+
78
+ const data = response.getData();
79
+ if (data?.result == null) {
80
+ throw new Error(`${methodName}: Bitrix24 вернул пустой ответ`);
81
+ }
82
+
83
+ return data.result as T;
84
+ }
85
+
58
86
  // ==================== Создание экземпляра B24OAuth ====================
59
87
 
60
88
  function createB24Instance(): B24OAuth | null {
@@ -91,7 +119,14 @@ function createB24Instance(): B24OAuth | null {
91
119
 
92
120
  const secret: B24OAuthSecret = { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET };
93
121
 
94
- return new B24OAuth(authParams, secret);
122
+ const b24 = new B24OAuth(authParams, secret);
123
+
124
+ // Логгер без хендлеров: SDK 2.x на каждый вызов callMethod/callBatch пишет
125
+ // предупреждение об устаревании напрямую в console.warn, если логгер — NullLogger.
126
+ // Любой другой логгер получает запись через свой интерфейс, поэтому консоль остаётся чистой.
127
+ b24.setLogger(Logger.create("npm-app-back-b24"));
128
+
129
+ return b24;
95
130
  }
96
131
 
97
132
  // ==================== Работа с токенами ====================
@@ -188,8 +223,9 @@ async function refreshAuthWithMutex(): Promise<AuthData> {
188
223
  lastError = error;
189
224
  if (!isB24NetworkError(error) || attempt === RETRY_COUNT) throw error;
190
225
  const msg = error instanceof Error ? error.message : String(error);
226
+ const code = error instanceof SdkError ? ` [${error.code}]` : "";
191
227
  const delayMs = RETRY_DELAY_MS * attempt;
192
- logs.add(`refreshAuth: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${delayMs}мс`, "error");
228
+ logs.add(`refreshAuth${code}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${delayMs}мс`, "error");
193
229
  await delay(delayMs);
194
230
  }
195
231
  }
@@ -261,15 +297,23 @@ export function stopProactiveRefresh(): void {
261
297
 
262
298
  /**
263
299
  * Проверяет, является ли ошибка из SDK сетевой.
264
- * AjaxError хранит AxiosError в поле originalError.
300
+ *
301
+ * SDK 2.x маскирует транспортные сбои: в логирующей ветке падает собственный
302
+ * TypeError, и наружу приходит AjaxError с кодом JSSDK_UNKNOWN_ERROR и status 0.
303
+ * Поэтому проверяем и код, и originalError, и status.
304
+ * RefreshTokenError приходит как SdkError с кодом вроде ENOTFOUND и без originalError.
265
305
  */
266
306
  function isB24NetworkError(error: unknown): boolean {
267
307
  if (error instanceof AjaxError) {
268
- // SDK уже сам retry'ил и исчерпал все попытки — не повторять
308
+ // Практически недостижимо (SDK бросает реальную ошибку раньше),
309
+ // но если код всё же пришёл — SDK уже исчерпал свои попытки
269
310
  if (error.code === "JSSDK_CALL_ALL_ATTEMPTS_EXHAUSTED") return false;
311
+ }
270
312
 
271
- const { originalError, status } = error;
313
+ if (error instanceof SdkError) {
314
+ const { code, originalError, status } = error;
272
315
 
316
+ if (SDK_NETWORK_CODES.has(code)) return true;
273
317
  if (originalError && isNetworkError(originalError)) return true;
274
318
  if (AXIOS_NETWORK_CODES.has((originalError as any)?.code)) return true;
275
319
  if (status === 0) return true;
@@ -294,7 +338,8 @@ function withRetry<T extends (...args: any[]) => Promise<any>>(fn: T, context: a
294
338
  }
295
339
 
296
340
  const msg = error instanceof Error ? error.message : String(error);
297
- logs.add(`$b24.${methodName}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${RETRY_DELAY_MS}мс`, "error");
341
+ const code = error instanceof SdkError ? ` [${error.code}]` : "";
342
+ logs.add(`$b24.${methodName}${code}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${RETRY_DELAY_MS}мс`, "error");
298
343
 
299
344
  await delay(RETRY_DELAY_MS);
300
345
  }
@@ -306,32 +351,36 @@ function withRetry<T extends (...args: any[]) => Promise<any>>(fn: T, context: a
306
351
  }
307
352
 
308
353
  /**
309
- * Proxy-обёртка вокруг B24OAuth: автоматически оборачивает
310
- * callMethod, callListMethod, callBatch и fetchListMethod retry-логикой.
311
- * Обёрнутые методы кешируются обёртка создаётся один раз на метод.
354
+ * Proxy-обёртка вокруг B24OAuth.
355
+ *
356
+ * Методы из RETRYABLE_METHODS оборачиваются retry-логикой, все остальные
357
+ * привязываются к оригинальному объекту через bind: класс B24OAuth использует
358
+ * приватные поля (#authOAuthManager), и при вызове метода с this === Proxy
359
+ * движок бросает "Cannot read private member".
360
+ *
361
+ * Обёртки кешируются — по одной на метод, чтобы не ломать сравнение по ссылке.
312
362
  */
313
363
  function wrapB24WithRetry(b24: B24OAuth): B24OAuth {
314
364
  const methodCache = new Map<string, Function>();
315
365
 
316
366
  return new Proxy(b24, {
317
- get(target, prop, receiver) {
318
- // Передаём target вместо receiver this внутри геттеров должен указывать
319
- // на оригинальный target, иначе приватные поля (#authOAuthManager и др.)
320
- // недоступны через Proxy
321
-
322
- // Для методов из RETRYABLE_METHODS оборачиваем retry-логикой
323
- if (typeof prop === "string" && RETRYABLE_METHODS.has(prop)) {
324
- const value = Reflect.get(target, prop, target);
325
- if (typeof value === "function") {
326
- if (!methodCache.has(prop)) {
327
- methodCache.set(prop, withRetry(value, target, prop));
328
- }
329
- return methodCache.get(prop);
330
- }
367
+ get(target, prop) {
368
+ // Передаём target третьим аргументом: геттеры (например, auth)
369
+ // тоже должны исполняться с this === target
370
+ const value = Reflect.get(target, prop, target);
371
+
372
+ if (typeof prop !== "string" || typeof value !== "function") {
373
+ return value;
374
+ }
375
+
376
+ if (!methodCache.has(prop)) {
377
+ const wrapped = RETRYABLE_METHODS.has(prop)
378
+ ? withRetry(value as (...args: any[]) => Promise<any>, target, prop)
379
+ : value.bind(target);
380
+ methodCache.set(prop, wrapped);
331
381
  }
332
382
 
333
- // Для остальных — передаём target чтобы приватные поля работали
334
- return Reflect.get(target, prop, target);
383
+ return methodCache.get(prop);
335
384
  },
336
385
  });
337
386
  }
@@ -1,5 +1,5 @@
1
1
  import { DateTime } from "luxon";
2
- import { $b24 } from "./b24.ts";
2
+ import { $b24, getResultData } from "./b24.ts";
3
3
  import { logs } from "../logs/logs.ts";
4
4
  import dotEnv from "dotenv";
5
5
 
@@ -54,8 +54,7 @@ export async function checkB24Scope(): Promise<void> {
54
54
 
55
55
  try {
56
56
  const scopeResponse = await $b24.callMethod("scope");
57
- const scopeData = scopeResponse.getData() as any;
58
- const scopes: string[] = scopeData.result ?? [];
57
+ const scopes = getResultData<string[]>(scopeResponse, "scope") ?? [];
59
58
 
60
59
  logs.add(`Права приложения Bitrix24: ${scopes.join(", ")}`, "info");
61
60
 
@@ -104,8 +103,7 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
104
103
  select: ["ID", "STATUS"],
105
104
  });
106
105
 
107
- const data = response.getData() as any;
108
- const tasks: TaskItem[] = data.result?.tasks ?? [];
106
+ const tasks: TaskItem[] = getResultData<{ tasks?: TaskItem[] }>(response, "tasks.task.list").tasks ?? [];
109
107
 
110
108
  if (tasks.length >= maxTasks) {
111
109
  return {
@@ -128,12 +126,12 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
128
126
  },
129
127
  });
130
128
 
131
- const createData = createResponse.getData() as any;
129
+ const createdTask = getResultData(createResponse, "tasks.task.add");
132
130
 
133
131
  return {
134
132
  error: false,
135
133
  message: "Задача создана в Битрикс24.",
136
- data: createData.result,
134
+ data: createdTask,
137
135
  };
138
136
  } catch (error: any) {
139
137
  return {
@@ -1,5 +1,5 @@
1
1
  import type { B24OAuth } from "@bitrix24/b24jssdk";
2
- import { $b24 } from "./b24.ts";
2
+ import { $b24, getResultData } from "./b24.ts";
3
3
 
4
4
  // ==================== Типы ====================
5
5
 
@@ -141,7 +141,9 @@ export class Event {
141
141
  filter: { EVENT_NAME: eventName },
142
142
  });
143
143
 
144
- const arrOfflineEvents = response.getData() as OfflineEventsResponse;
144
+ const arrOfflineEvents = getResultData<OfflineEventsResponse["result"]>(response, "event.offline.get");
145
+ // B24 для пустой коллекции может вернуть result: [] — тогда events отсутствует
146
+ const allEvents = arrOfflineEvents?.events ?? [];
145
147
 
146
148
  // Обработка событий коннектора
147
149
  if (eventName === "ONIMCONNECTORMESSAGEADD") {
@@ -150,9 +152,9 @@ export class Event {
150
152
  message: [],
151
153
  };
152
154
 
153
- if (arrOfflineEvents.result.events.length > 0) {
154
- data.processId = arrOfflineEvents.result.process_id;
155
- data.message = arrOfflineEvents.result.events.map((event) => {
155
+ if (allEvents.length > 0) {
156
+ data.processId = arrOfflineEvents.process_id;
157
+ data.message = allEvents.map((event) => {
156
158
  const messageData = event.EVENT_DATA.MESSAGES[0];
157
159
 
158
160
  return {
@@ -178,23 +180,23 @@ export class Event {
178
180
  };
179
181
 
180
182
  // Очищаем события от системного пользователя
181
- const arrEventsToClear = arrOfflineEvents.result.events.filter((event) => event.EVENT_ADDITIONAL.user_id === SYSTEM_USER_ID);
183
+ const arrEventsToClear = allEvents.filter((event) => event.EVENT_ADDITIONAL.user_id === SYSTEM_USER_ID);
182
184
 
183
185
  if (arrEventsToClear.length > 0) {
184
186
  const arrMessageIdToClear = arrEventsToClear.map((event) => event.MESSAGE_ID);
185
187
  await this.b24.callMethod("event.offline.clear", {
186
- process_id: arrOfflineEvents.result.process_id,
188
+ process_id: arrOfflineEvents.process_id,
187
189
  message_id: arrMessageIdToClear,
188
190
  });
189
-
190
- // Удаляем эти события из основного массива
191
- arrOfflineEvents.result.events = arrOfflineEvents.result.events.filter((event) => event.EVENT_ADDITIONAL.user_id !== SYSTEM_USER_ID);
192
191
  }
193
192
 
194
- if (arrOfflineEvents.result.events.length > 0) {
195
- data.processId = arrOfflineEvents.result.process_id;
196
- data.entitysId = arrOfflineEvents.result.events.map((event) => event.EVENT_DATA.FIELDS.ID);
197
- data.arrMessageIdAndEntityId = arrOfflineEvents.result.events.map((event) => ({
193
+ // Работаем только с событиями, не относящимися к системному пользователю
194
+ const activeEvents = allEvents.filter((event) => event.EVENT_ADDITIONAL.user_id !== SYSTEM_USER_ID);
195
+
196
+ if (activeEvents.length > 0) {
197
+ data.processId = arrOfflineEvents.process_id;
198
+ data.entitysId = activeEvents.map((event) => event.EVENT_DATA.FIELDS.ID);
199
+ data.arrMessageIdAndEntityId = activeEvents.map((event) => ({
198
200
  messageId: event.MESSAGE_ID,
199
201
  entityId: event.EVENT_DATA.FIELDS.ID,
200
202
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrey4emk/npm-app-back-b24",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Bitrix24 OAuth helpers for Node.js projects",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -35,15 +35,14 @@
35
35
  "utils/fetchRetry.ts"
36
36
  ],
37
37
  "dependencies": {
38
- "@bitrix24/b24jssdk": "^1.0.4",
39
- "@types/express": "^5.0.6",
40
- "@types/luxon": "^3.7.1",
41
- "@types/node": "^25.0.10",
42
- "conf": "^15.0.2",
43
- "dotenv": "^17.2.3",
44
- "luxon": "^3.4.4",
45
- "nodemailer": "^7.0.6",
46
- "@types/nodemailer": "^7.0.9"
47
- },
48
- "devDependencies": {}
38
+ "@bitrix24/b24jssdk": "2.0.0",
39
+ "@types/express": "5.0.6",
40
+ "@types/luxon": "3.7.1",
41
+ "@types/node": "25.0.10",
42
+ "@types/nodemailer": "7.0.9",
43
+ "conf": "15.0.2",
44
+ "dotenv": "17.2.3",
45
+ "luxon": "3.4.4",
46
+ "nodemailer": "7.0.6"
47
+ }
49
48
  }
@@ -1,4 +1,5 @@
1
1
  import type { B24OAuth } from "@bitrix24/b24jssdk";
2
+ import { getResultData } from "../bitrix24/b24.ts";
2
3
 
3
4
  // ==================== Типы ====================
4
5
 
@@ -74,10 +75,10 @@ export class Smsgold {
74
75
 
75
76
  // Получаем публичную ссылку на файл для вставки в сообщение
76
77
  try {
77
- let resLink = await this.b24!.callMethod("disk.file.getExternalLink", {
78
+ const resLink = await this.b24!.callMethod("disk.file.getExternalLink", {
78
79
  id: idFileContract,
79
80
  });
80
- publicLink = (resLink as any).getData().result;
81
+ publicLink = getResultData<string>(resLink, "disk.file.getExternalLink");
81
82
  } catch (error: unknown) {
82
83
  const errMsg = error instanceof Error ? error.message : String(error);
83
84
  return { error: true, message: `Ошибка при получении публичной ссылки на файл: ${errMsg}` };
@@ -139,7 +140,7 @@ export class Smsgold {
139
140
  data: { NAME: fileName },
140
141
  fileContent: [fileName, base64File],
141
142
  });
142
- const data = (res as any).getData().result;
143
+ const data = getResultData(res, "disk.folder.uploadfile");
143
144
  return { error: false, data };
144
145
  } catch (error: unknown) {
145
146
  const errMsg = error instanceof Error ? error.message : String(error);