@andrey4emk/npm-app-back-b24 3.7.1 → 3.8.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 +123 -11
- package/bitrix24/b24.ts +103 -20
- package/logs/logs.ts +19 -1
- package/package.json +5 -2
- package/sendMessage/chatApp.ts +192 -95
- package/sendMessage/email.ts +11 -2
- package/sendMessage/smsgold.ts +57 -12
- package/sendMessage/wappi.ts +144 -53
- package/utils/fetchRetry.ts +251 -8
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- [Email](#email)
|
|
16
16
|
- [Wappi](#wappi)
|
|
17
17
|
- [Логирование](#логирование)
|
|
18
|
-
- [fetchRetry](#fetchretry)
|
|
18
|
+
- [fetchRetry и таймауты](#fetchretry-и-таймауты)
|
|
19
19
|
- [Переменные окружения](#переменные-окружения)
|
|
20
20
|
- [Скрипты](#скрипты)
|
|
21
21
|
- [Лицензия](#лицензия)
|
|
@@ -32,7 +32,7 @@ npm install @andrey4emk/npm-app-back-b24
|
|
|
32
32
|
| --------------------------------------------- | ----------------------------- | ---------------------- |
|
|
33
33
|
| `@andrey4emk/npm-app-back-b24` | всё | да |
|
|
34
34
|
| `@andrey4emk/npm-app-back-b24/logs` | `logs` | нет |
|
|
35
|
-
| `@andrey4emk/npm-app-back-b24/fetchRetry` | `fetchRetry`, `isNetworkError`, `isPreConnectionError`, `maskUrl` | нет |
|
|
35
|
+
| `@andrey4emk/npm-app-back-b24/fetchRetry` | `fetchRetry`, `fetchWithTimeout`, `isNetworkError`, `isPreConnectionError`, `isTimeoutError`, `isAbortError`, `maskUrl`, `FETCH_TIMEOUTS`, `DEFAULT_FETCH_TIMEOUT_MS` | нет |
|
|
36
36
|
|
|
37
37
|
Корневой импорт — barrel: он тянет модуль OAuth, который при загрузке читает `authB24.json` и, если приложение авторизовано, запускает таймер проактивного обновления токена. Проектам, которые ходят в B24 по **входящему вебхуку** или берут из пакета только логгер и `fetchRetry`, удобнее импортировать по подпутям — тогда OAuth-модуль не загружается вообще.
|
|
38
38
|
|
|
@@ -50,6 +50,41 @@ import { fetchRetry, maskUrl } from "@andrey4emk/npm-app-back-b24/fetchRetry";
|
|
|
50
50
|
|
|
51
51
|
Практическое следствие: индексация массива внутри пакета всегда сопровождается проверкой на `undefined`. Если в своём проекте эти флаги включены — ошибок из `node_modules/@andrey4emk/npm-app-back-b24` быть не должно.
|
|
52
52
|
|
|
53
|
+
### Nuxt: сборка в Nitro и импорт `$b24`
|
|
54
|
+
|
|
55
|
+
Пакет публикуется исходниками на TypeScript, а Nitro по умолчанию не транспилирует ничего из `node_modules`. Двух ловушек здесь достаточно, чтобы потерять полдня, — обе проверены на Nuxt 4.5.
|
|
56
|
+
|
|
57
|
+
**Сборка.** Одного `nitro.externals.inline` мало: Nuxt жёстко ставит `esbuild.options.exclude = [/node_modules/]`, а `defu` при слиянии конфигов массивы **склеивает**, а не заменяет — переопределить это из `nuxt.config` нельзя. Rollup получает сырой TypeScript и падает на `Expected '{', got 'interface'`. Рабочая комбинация — inline плюс хук `nitro:config`, который присваивает `exclude` заново:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// nuxt.config.ts
|
|
61
|
+
export default defineNuxtConfig({
|
|
62
|
+
nitro: { externals: { inline: ["@andrey4emk/npm-app-back-b24"] } },
|
|
63
|
+
hooks: {
|
|
64
|
+
"nitro:config"(nitroConfig) {
|
|
65
|
+
// ЗАМЕНЯЕТ exclude, а не дополняет — иначе /node_modules/ из дефолта победит
|
|
66
|
+
nitroConfig.esbuild.options.exclude = [/node_modules\/(?!@andrey4emk\/)/];
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Импорт `$b24`.** Это `export let`, а не `const`: `reinitializeB24()` переприсваивает биндинг. Захват значения в момент импорта (`const b24 = $b24` на уровне модуля) навсегда оставит то, что было при загрузке, — как правило `null`, потому что токены к этому моменту ещё не прочитаны. Читать свойство нужно в момент вызова:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// server/utils/b24.ts
|
|
76
|
+
import { $b24 } from "@andrey4emk/npm-app-back-b24";
|
|
77
|
+
|
|
78
|
+
// Геттер, а не const: биндинг $b24 меняется после reinitializeB24()
|
|
79
|
+
export const getB24 = () => $b24;
|
|
80
|
+
|
|
81
|
+
// Использование
|
|
82
|
+
const b24 = getB24();
|
|
83
|
+
if (b24) {
|
|
84
|
+
const res = await b24.callMethod("crm.deal.get", { id: 123 });
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
53
88
|
## Использование
|
|
54
89
|
|
|
55
90
|
```js
|
|
@@ -70,8 +105,11 @@ import {
|
|
|
70
105
|
Wappi,
|
|
71
106
|
logs,
|
|
72
107
|
fetchRetry,
|
|
108
|
+
fetchWithTimeout,
|
|
73
109
|
isNetworkError,
|
|
110
|
+
isTimeoutError,
|
|
74
111
|
maskUrl,
|
|
112
|
+
FETCH_TIMEOUTS,
|
|
75
113
|
} from "@andrey4emk/npm-app-back-b24";
|
|
76
114
|
|
|
77
115
|
// $b24 — готовый экземпляр B24OAuth (или null, если токены не настроены)
|
|
@@ -119,8 +157,11 @@ await wappi.sendMessageWappi("whatsApp", { phone: "+1234567890", message: "Пр
|
|
|
119
157
|
// Логирование
|
|
120
158
|
logs.add("Сообщение", "info");
|
|
121
159
|
|
|
122
|
-
// fetch с повторными попытками при сетевых ошибках
|
|
160
|
+
// fetch с повторными попытками при сетевых ошибках (бюджет попытки — 60с)
|
|
123
161
|
const response = await fetchRetry("https://api.example.com/data", { method: "GET" });
|
|
162
|
+
|
|
163
|
+
// Один запрос без повторов — для неидемпотентных операций
|
|
164
|
+
const sent = await fetchWithTimeout("https://api.example.com/send", { method: "POST" }, FETCH_TIMEOUTS.send);
|
|
124
165
|
```
|
|
125
166
|
|
|
126
167
|
## API
|
|
@@ -167,15 +208,21 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
167
208
|
|
|
168
209
|
**Уровни логов.** Промежуточные попытки пишутся на `warn`, окончательный отказ — на `error`: исчерпание всех попыток и отмена повтора гейтом. Так один упавший вызов даёт одно сообщение в чат B24 вместо пяти, но при этом не теряется совсем. Логировать провал обязан сам Proxy: `errorB24()`, `Event` и `Smsgold` ошибку только возвращают вызывающему коду и в лог не пишут.
|
|
169
210
|
|
|
170
|
-
**Создающие вызовы не повторяются Proxy-слоем.** Если в вызове есть метод, создающий сущность или запускающий действие (`*.add`, `*.create`, `*.start`, `*.send`, `*.uploadfile`, `*.import`, `*.register` — в том числе среди команд `callBatch`), retry отменяется, а в лог идёт `повтор отменён`. Причина:
|
|
211
|
+
**Создающие вызовы не повторяются Proxy-слоем.** Если в вызове есть метод, создающий сущность или запускающий действие (`*.add`, `*.create`, `*.start`, `*.send`, `*.uploadfile`, `*.import`, `*.register` — в том числе среди команд `callBatch`), retry отменяется, а в лог идёт `повтор отменён`. Причина: транспортная ошибка не говорит, дошёл ли запрос до портала. `NETWORK_ERROR` приходит и когда соединение не состоялось, и когда оно оборвалось после того, как портал уже принял и выполнил запрос — а повтор во втором случае создаёт вторую задачу, второй файл, второй экземпляр бизнес-процесса. Идемпотентные записи (`*.update`, `*.delete`, `*.set`) повторяются как прежде: повторное применение даёт то же состояние.
|
|
171
212
|
|
|
172
213
|
Команды `callBatch` разбираются в обеих формах — кортеж `["crm.deal.add", {...}]` (основная в SDK 2.x) и объект `{ method, params }`. Если форму команды разобрать не удалось, вызов считается создающим и не повторяется: для защиты от дубликатов безопаснее ошибиться в сторону осторожности.
|
|
173
214
|
|
|
174
|
-
Важно:
|
|
215
|
+
Важно: **транспортные сбои SDK не повторяет вообще** — пакет задаёт параметры ограничителя при создании клиента. Иначе гейт был бы дырявым: SDK не различает идемпотентные и создающие вызовы и повторил бы оборванный `crm.deal.add` до трёх раз внутри себя, куда наша защита не дотягивается. Измерено: без этих параметров обрыв соединения, 502 и 504 дают по три выполнения на портале, с ними — по одному.
|
|
216
|
+
|
|
217
|
+
Повторы SDK при 429 и 503 сохранены: портал отвечает отказом, не выполнив запрос.
|
|
218
|
+
|
|
219
|
+
Отсюда следствие для вашего кода: **не вызывайте `setRestrictionManagerParams()` и не поднимайте `initB24Helper()` / `B24HelperManager` на `$b24`** — они перезаписывают настройки ограничителя целиком, не сливая, и молча вернут повторы SDK вместе с риском дубликатов.
|
|
175
220
|
|
|
176
221
|
**Обмен refresh-токена** повторяется по более строгому правилу — только если соединение заведомо не состоялось (см. `isPreConnectionError()`). При таймауте или обрыве сервер мог уже провести ротацию; тогда локальный `refresh_token` мёртв независимо от наших действий, и повтор не помогает, а лишь маскирует проблему серией одинаковых отказов. Следующая попытка всё равно будет: проактивный таймер повторяет через 2 минуты при времени жизни токена около часа.
|
|
177
222
|
|
|
178
|
-
У самого SDK есть собственный слой ретраев (`maxRetries: 3`), но
|
|
223
|
+
У самого SDK есть собственный слой ретраев (`maxRetries: 3`), но на транспортных ошибках он выключен — там одна попытка, повторяет только Proxy. При 429, 503 и неизвестных 5xx SDK повторяет по-прежнему.
|
|
224
|
+
|
|
225
|
+
Отдельно про ответы шлюза перед порталом. **502** повторяет наш слой: шлюз не смог получить ответ от upstream, запрос почти наверняка не выполнялся. **504** не повторяет никто — это «портал взял запрос и считает прямо сейчас»; повтор создающего вызова дал бы дубликат, а читающего — ничего, кроме нагрузки на портал в худший для него момент.
|
|
179
226
|
|
|
180
227
|
**Диагностика SDK.** Начиная с 3.7.0 собственные сообщения SDK от уровня `WARNING` и выше попадают в лог пакета с префиксом `SDK B24:` вместе с контекстом (`requestId`, `method`, `status`, код ошибки) — например предупреждение `fetchList` про игнорируемый `order` или про остановку пагинации, когда `idKey` не совпал с полем ответа. Порог нужен: на уровне `info` SDK пишет две строки на каждый запрос. Ошибки SDK приходят как `warn`, а не `error` — это диагностика отдельной неуспешной попытки, а окончательный провал вызова один раз пишет retry-слой.
|
|
181
228
|
|
|
@@ -501,6 +548,8 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
501
548
|
|
|
502
549
|
Конструктор сразу разрешает тип мессенджера для `whatsApp` и `telegram` — берёт `messenger[0].type` и держит в полях, вместо того чтобы индексировать массив при каждой отправке. Если конфига нет или список `messenger` пуст, тип остаётся неразрешённым, и **методы этого мессенджера возвращают** `{ error: true, message: "ChatApp: не разрешён тип мессенджера whatsApp (нет конфига или пуст messenger)", data: null }` — вместо запроса на URL с `undefined` в пути. Конструктор при этом **не бросает**: экземпляр обычно создаётся на уровне модуля, и бросок уронил бы загрузку всего модуля отправки вместе с соседними каналами.
|
|
503
550
|
|
|
551
|
+
**Таймаут отправки — статус неизвестен.** Запросы идут с бюджетом времени (10 секунд на работу с токеном, 20 на проверку номера, 30 на отправку сообщения, 60 на отправку файла). Если ответа не дождались, отправка **могла состояться** — методы возвращают `{ error: true, message: "ChatApp: таймаут ... — статус неизвестен", data: null }`, а не бросают исключение. Повторять такую отправку нельзя: клиент получит второе сообщение. Пакет дополнительно пишет строку уровня `error` (она уходит в чат B24) — решение о повторе принимает человек. Раньше зависание долетало до вызывающего кода исключением через пять минут; теперь оно возвращается объектом через тридцать секунд, поэтому запись в лог обязательна: без неё отказ стал бы тише, чем был.
|
|
552
|
+
|
|
504
553
|
Отказ локальный: пустой `whatsApp` не мешает `telegram`. Но конфиг всё равно нужен для **обоих** мессенджеров, даже если проект пользуется одним — иначе второй канал молча перестанет работать, и узнать об этом можно будет только по `res.error` в логе.
|
|
505
554
|
|
|
506
555
|
**Методы:**
|
|
@@ -564,6 +613,7 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
564
613
|
- `messengerType` (string) — `"whatsApp"` или `"telegram"`.
|
|
565
614
|
- `phone` (string) — номер телефона.
|
|
566
615
|
- **Возвращает:** промис с объектом `{ error, message, data }`, где `data.check` — результат проверки.
|
|
616
|
+
- **Особенности:** если API ответил `success: true`, но без объекта `data`, метод возвращает `{ error: true, message: "Ответ ChatApp без поля data ...", data }` с сырым ответом. Раньше в этом случае наружу летел `TypeError` — метод не обёрнут в `try/catch`. Подставлять `check: undefined` было бы хуже: вызывающий принял бы непонятный ответ за «телефон не найден».
|
|
567
617
|
|
|
568
618
|
### Smsgold
|
|
569
619
|
|
|
@@ -574,10 +624,13 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
574
624
|
|
|
575
625
|
const smsClient = new Smsgold(
|
|
576
626
|
{ user: "smsgold_login", pass: "smsgold_password" },
|
|
577
|
-
b24Instance // опционально — экземпляр B24OAuth для загрузки файлов на диск
|
|
627
|
+
b24Instance, // опционально — экземпляр B24OAuth для загрузки файлов на диск
|
|
628
|
+
123456 // опционально — ID папки на диске B24 для вложений, по умолчанию 2792881
|
|
578
629
|
);
|
|
579
630
|
```
|
|
580
631
|
|
|
632
|
+
Третий параметр — папка, куда кладётся файл из `fileUrl` перед отправкой SMS. Дефолт `2792881` — папка «contract_company» портала `repacheb.bitrix24.ru`; на другом портале такого ID нет, и его нужно передать своим. Параметр нужен только при отправке файлов.
|
|
633
|
+
|
|
581
634
|
**Методы:**
|
|
582
635
|
|
|
583
636
|
- **`sendSms(messageData)`** — отправить SMS.
|
|
@@ -588,7 +641,7 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
588
641
|
- `message` (string) — текст сообщения.
|
|
589
642
|
- `fileUrl` (string) — опционально, ссылка на файл (будет загружен на диск Bitrix24 и публичная ссылка добавлена в SMS).
|
|
590
643
|
- `fileName` (string) — опционально, имя файла.
|
|
591
|
-
- **Возвращает:** промис с объектом `{ error, message, result }`.
|
|
644
|
+
- **Возвращает:** промис с объектом `{ error, message, result }`, а при таймауте — ещё и `unknownDelivery: true`.
|
|
592
645
|
|
|
593
646
|
```js
|
|
594
647
|
const result = await smsClient.sendSms({
|
|
@@ -600,6 +653,19 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
600
653
|
}
|
|
601
654
|
```
|
|
602
655
|
|
|
656
|
+
- **Таймаут и неизвестный статус доставки.** Отправка идёт без повторов с бюджетом 30 секунд. Если SMSGold не ответил, запрос уже ушёл — сообщение **могло быть доставлено**, и повторять отправку нельзя. Такой результат отличается от обычной ошибки: `unknownDelivery: true` и текст «SMSGold не ответил за 30с — статус отправки неизвестен». Дополнительно пакет пишет строку уровня `error` (она уходит в чат B24), потому что решение о повторе должен принимать человек, а не код. Это единственное место, где `Smsgold` пишет в лог сам.
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
const result = await smsClient.sendSms({ phone, message });
|
|
660
|
+
if (result.unknownDelivery) {
|
|
661
|
+
// Не отправлять повторно: возможен дубль у клиента. Ставим задачу на ручную проверку
|
|
662
|
+
} else if (result.error) {
|
|
663
|
+
// Обычная ошибка — отправка не состоялась, повтор безопасен
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
- **Скачивание файла** по `fileUrl` идёт через `fetchRetry` с 3 попытками и бюджетом 60 секунд на попытку (раньше было 5 попыток без таймаута): пять попыток по минуте задерживали бы саму отправку SMS до пяти минут.
|
|
668
|
+
|
|
603
669
|
### Email
|
|
604
670
|
|
|
605
671
|
- **`Email`** — класс для отправки email через SMTP Яндекса. Экземпляр нужно создать самостоятельно.
|
|
@@ -640,12 +706,15 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
640
706
|
- SMTP-сервер: `smtp.yandex.ru:465` (SMTPS).
|
|
641
707
|
- Копия письма автоматически отправляется на адрес отправителя.
|
|
642
708
|
- Транспорт — `nodemailer` `^9` (диапазон, а не точная версия: иначе патч безопасности нельзя было бы применить на стороне потребителя).
|
|
643
|
-
- Вложения передаются готовым `Buffer` — файл по `fileUrl` пакет скачивает сам. Поля `path` и `href` намеренно не поддерживаются: nodemailer не должен ходить по URL.
|
|
709
|
+
- Вложения передаются готовым `Buffer` — файл по `fileUrl` пакет скачивает сам. Поля `path` и `href` намеренно не поддерживаются: nodemailer не должен ходить по URL. Скачивание идёт через `fetchWithTimeout` с бюджетом 60 секунд.
|
|
710
|
+
- Таймауты SMTP-транспорта переопределены: соединение 15 с, приветствие 10 с, сокет 120 с. Дефолты nodemailer (2 мин / 30 с / 10 мин) слишком щедры — письмо на Яндекс уходит за секунды, а 10 минут молчания сокета блокируют вызывающую очередь.
|
|
644
711
|
|
|
645
712
|
### Wappi
|
|
646
713
|
|
|
647
714
|
- **`Wappi`** — класс для отправки сообщений через Wappi API (WhatsApp, Telegram, Max). Экземпляр нужно создать самостоятельно.
|
|
648
715
|
|
|
716
|
+
**Таймаут отправки — статус неизвестен.** Как и у `ChatApp`: бюджет 20 секунд на проверку номера, 30 на сообщение, 60 на файл. При таймауте отправки метод возвращает `{ error: true, message: "Wappi: таймаут ... — статус неизвестен", data: null }` и пишет строку уровня `error` в чат B24 — сообщение могло уйти, повторять нельзя. Отдельно про Telegram: `phoneCheckWappi` при таймауте проверки контакта **не создаёт контакт**, а возвращает ошибку. Иначе не дождавшись ответа мы бы завели контакт на номер, который мог уже быть в адресной книге, и объявили номер проверенным без проверки.
|
|
717
|
+
|
|
649
718
|
```js
|
|
650
719
|
import { Wappi } from "@andrey4emk/npm-app-back-b24";
|
|
651
720
|
|
|
@@ -720,9 +789,9 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
720
789
|
- Конфигурация хранится в файле `log.json` (через `conf`).
|
|
721
790
|
- Каждый уровень можно включить/выключить, изменить цвет.
|
|
722
791
|
|
|
723
|
-
### fetchRetry
|
|
792
|
+
### fetchRetry и таймауты
|
|
724
793
|
|
|
725
|
-
- **`fetchRetry(url, options?, retries?, delay?)`** — обёртка над `fetch` с повторными попытками при сетевых ошибках. HTTP-ошибки (4xx, 5xx) **не** вызывают повторных попыток — повторяются только сетевые сбои (TypeError, ECONNRESET, ETIMEDOUT и т.д.)
|
|
794
|
+
- **`fetchRetry(url, options?, retries?, delay?, timeoutMs?)`** — обёртка над `fetch` с повторными попытками при сетевых ошибках. HTTP-ошибки (4xx, 5xx) **не** вызывают повторных попыток — повторяются только сетевые сбои (TypeError, ECONNRESET, ETIMEDOUT и т.д.) и собственный таймаут запроса.
|
|
726
795
|
|
|
727
796
|
- **`maskUrl(url)`** — маскирует секреты в адресе перед записью в лог. URL входящего вебхука B24 содержит секрет прямо в пути (`/rest/1/<секрет>/method.json`), а токен авторизации может приходить в query-параметрах (`auth`, `access_token`, `refresh_token`, `token`). Хост и имя метода сохраняются, тело секрета заменяется на `***`. Используется внутри `fetchRetry`; применяйте в своём коде везде, где логируете адреса запросов к B24.
|
|
728
797
|
|
|
@@ -769,6 +838,7 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
769
838
|
| `options` | RequestInit | `undefined` | Параметры fetch (метод, заголовки, body и т.д.) |
|
|
770
839
|
| `retries` | number | `5` | Количество попыток |
|
|
771
840
|
| `delay` | number | `500` | Задержка между попытками (мс) |
|
|
841
|
+
| `timeoutMs` | number | `60000` (или `FETCH_TIMEOUT_MS`) | Бюджет на одну попытку; `0` — без таймаута |
|
|
772
842
|
|
|
773
843
|
- **Возвращает:** `Promise<Response>` — ответ от fetch.
|
|
774
844
|
|
|
@@ -797,6 +867,46 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
797
867
|
);
|
|
798
868
|
```
|
|
799
869
|
|
|
870
|
+
#### Таймауты
|
|
871
|
+
|
|
872
|
+
С версии 3.8.0 каждый запрос пакета имеет бюджет времени: зависшее соединение больше не держится до таймаутов undici (5 минут на попытку, а при медленной «капле» данных — бесконечно).
|
|
873
|
+
|
|
874
|
+
- **Бюджет считается на попытку, а не на весь вызов.** Каждой попытке `fetchRetry` выдаётся свой `AbortSignal.timeout(timeoutMs)`. Бюджет покрывает и чтение тела ответа: сигнал живёт до конца запроса, поэтому `res.text()` / `res.arrayBuffer()` могут упасть с `TimeoutError` уже **после** возврата из `fetchRetry`. Для крупных файлов передавайте больший `timeoutMs` или `0`.
|
|
875
|
+
- **Худший случай по времени** — `retries × (timeoutMs + delay)`. При дефолтах это 5 × (60 000 + 500) ≈ 5 минут.
|
|
876
|
+
- **Свой `options.signal` отключает наш дефолт.** Если вызывающий передал сигнал именно в `options`, а `timeoutMs` не задан, пакет своего таймаута не добавляет — чужой осознанный бюджет не обрезается. Явно переданный `timeoutMs` применяется всегда, тогда побеждает тот сигнал, который сработал раньше. Отмена по чужому сигналу **никогда не повторяется**; различаем её по объекту сигнала, а не по типу ошибки: причиной отмены может быть любой объект, включая `TypeError`.
|
|
877
|
+
- **Сигнал у объекта `Request` на дефолт не влияет.** Он участвует в отмене — сигналы склеиваются через `AbortSignal.any`, иначе `fetch(request, init)` затёр бы его, — но признаком «вызывающий сам управляет временем» не считается: у любого `Request` сигнал есть всегда, даже когда его никто не задавал. Учитывать его значило бы молча снять таймаут со всех вызовов такой формы. Нужен свой бюджет при вызове с `Request` — передавайте `timeoutMs` пятым аргументом.
|
|
878
|
+
- **Таймаут при чтении тела не повторяется и не логируется.** Бюджет действует до конца запроса, поэтому `res.text()` / `res.arrayBuffer()` после возврата из `fetchRetry` могут упасть с `TimeoutError` уже у вас. Функция об этом не знает: повторов там нет, записи в лог тоже. Читаете большое тело — задавайте `timeoutMs` с запасом на скачивание.
|
|
879
|
+
- **Переменная `FETCH_TIMEOUT_MS`** меняет дефолт без правки кода (читается один раз при загрузке модуля), `FETCH_TIMEOUT_MS=0` выключает таймаут. Это аварийный рычаг на случай, когда дефолт обрывает живой долгий запрос — отчёт Seatable, Google Sheets, МойСклад. Действует только на вызовы **без явного** `timeoutMs`: внутренние вызовы пакета (ChatApp, Wappi, SMS, скачивание файлов) передают свои значения из `FETCH_TIMEOUTS` и переменную игнорируют. Значение должно быть целым от 0 до 2 147 483 647 — непригодное отбрасывается с записью в лог, а не выключает защиту молча.
|
|
880
|
+
- **Ограничение при вызове с объектом `Request`:** тело такого запроса читается первой попыткой, и повтор упадёт с `body used already`. Существовало и до таймаутов; для повторяемых запросов передавайте строку URL и `options`.
|
|
881
|
+
|
|
882
|
+
- **`fetchWithTimeout(url, options?, timeoutMs?)`** — один запрос с бюджетом времени, **без повторов и без логирования**. Для неидемпотентных операций: отправка сообщения, создание сущности — там, где повтор мог бы продублировать действие, но защита от зависшего соединения нужна. Правила про чужой сигнал те же, что у `fetchRetry`. Внутри пакета через неё идут все отправки ChatApp, Wappi и SMSGold.
|
|
883
|
+
|
|
884
|
+
```js
|
|
885
|
+
import { fetchWithTimeout, FETCH_TIMEOUTS } from "@andrey4emk/npm-app-back-b24/fetchRetry";
|
|
886
|
+
|
|
887
|
+
const res = await fetchWithTimeout(
|
|
888
|
+
"https://api.example.com/orders",
|
|
889
|
+
{ method: "POST", body: JSON.stringify(order) },
|
|
890
|
+
FETCH_TIMEOUTS.send // 30 000 мс
|
|
891
|
+
);
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
- **`FETCH_TIMEOUTS`** — именованные бюджеты, которыми пакет пользуется сам: `quick` 10 000 (токены), `api` 20 000 (короткое чтение), `send` 30 000 (отправка), `transfer` 60 000 (файлы). **`DEFAULT_FETCH_TIMEOUT_MS`** — действующий дефолт `fetchRetry` (60 000 либо значение `FETCH_TIMEOUT_MS`).
|
|
895
|
+
|
|
896
|
+
- **`isTimeoutError(error)` / `isAbortError(error)`** — различают «не дождались» и «отменили». Ошибка таймаута не оборачивается в свою: наружу идёт исходный `DOMException` с `name: "TimeoutError"` (отмена без причины — `"AbortError"`), проверки читают именно `name`.
|
|
897
|
+
|
|
898
|
+
```js
|
|
899
|
+
import { fetchRetry, isTimeoutError, isAbortError } from "@andrey4emk/npm-app-back-b24/fetchRetry";
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
await fetchRetry(url, { signal: ac.signal }, 3, 500, 20_000);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
if (isTimeoutError(error)) logs.add("не дождались ответа", "warn");
|
|
905
|
+
else if (isAbortError(error)) logs.add("запрос отменили", "debug");
|
|
906
|
+
else throw error;
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
800
910
|
## Переменные окружения
|
|
801
911
|
|
|
802
912
|
| Переменная | Назначение |
|
|
@@ -807,6 +917,7 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
807
917
|
| `APP_ENV` | Определяет окружение, используется как ключ секции авторизации |
|
|
808
918
|
| `APP_NAME` | Название приложения (используется в описании задач) |
|
|
809
919
|
| `CONFIG_DIR` | Путь к директории с конфигами (по умолчанию `../config`) |
|
|
920
|
+
| `FETCH_TIMEOUT_MS` | Бюджет одной попытки `fetchRetry`/`fetchWithTimeout`, мс (по умолчанию `60000`; `0` — без таймаута) |
|
|
810
921
|
| `CHATAPP_EMAIL` | Email аккаунта ChatApp |
|
|
811
922
|
| `CHATAPP_PASS` | Пароль аккаунта ChatApp |
|
|
812
923
|
| `CHATAPP_APP_ID` | ID приложения в ChatApp |
|
|
@@ -820,6 +931,7 @@ APP_B24_CLIENT_ID=xxx
|
|
|
820
931
|
APP_B24_CLIENT_SECRET=yyy
|
|
821
932
|
APP_NAME=MyApp
|
|
822
933
|
CONFIG_DIR=../config
|
|
934
|
+
FETCH_TIMEOUT_MS=60000
|
|
823
935
|
|
|
824
936
|
CHATAPP_EMAIL=your-email@example.com
|
|
825
937
|
CHATAPP_PASS=your-password
|
package/bitrix24/b24.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
Handler,
|
|
12
12
|
LogRecord,
|
|
13
13
|
Formatter,
|
|
14
|
+
RestrictionParams,
|
|
14
15
|
} from "@bitrix24/b24jssdk";
|
|
15
16
|
import { logs } from "../logs/logs.ts";
|
|
16
17
|
import { isNetworkError, isPreConnectionError } from "../utils/fetchRetry.ts";
|
|
@@ -114,14 +115,57 @@ const NETWORK_ERROR_CODES = new Set(["NETWORK_ERROR", "REQUEST_TIMEOUT", "ERR_NE
|
|
|
114
115
|
* REST-методы B24, повтор которых создаёт дубликат сущности или запускает
|
|
115
116
|
* повторное действие: add, create, start, send, uploadfile, import, register.
|
|
116
117
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
118
|
+
* Транспортная ошибка не говорит, дошёл ли запрос до портала: NETWORK_ERROR приходит
|
|
119
|
+
* и когда соединение не состоялось, и когда оно оборвалось после того, как портал уже
|
|
120
|
+
* принял и выполнил запрос. Во втором случае retry создаст вторую задачу, второй файл,
|
|
121
|
+
* второй экземпляр бизнес-процесса. Идемпотентные записи (update, delete, set) повторять
|
|
122
|
+
* безопасно: повторное применение даёт то же состояние.
|
|
123
|
+
*
|
|
124
|
+
* По той же причине у SDK отключены собственные повторы транспортных сбоев
|
|
125
|
+
* (SDK_RESTRICTION_PARAMS) — он этой разницы не знает вовсе.
|
|
122
126
|
*/
|
|
123
127
|
const NON_IDEMPOTENT_METHOD_RE = /\.(add|create|start|send|uploadfile|import|register)(\.json)?(\?|$)/i;
|
|
124
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Параметры ограничителя SDK. Передаются третьим аргументом конструктора B24OAuth
|
|
131
|
+
* и сливаются там с дефолтами `ParamsFactory.getDefault()`.
|
|
132
|
+
*
|
|
133
|
+
* Единый инвариант: **транспортные сбои SDK не повторяет вообще, повторяет только
|
|
134
|
+
* наш слой**, где работает гейт NON_IDEMPOTENT_METHOD_RE. У SDK нет понятия
|
|
135
|
+
* «создающий вызов»: оборванный crm.deal.add он повторил бы трижды и создал три сделки.
|
|
136
|
+
*
|
|
137
|
+
* Одного `retryOnNetworkError: false` для этого мало. Он добавляет в жёсткий список
|
|
138
|
+
* ровно два кода — NETWORK_ERROR и REQUEST_TIMEOUT, — а SDK конвертирует в них только
|
|
139
|
+
* `ERR_NETWORK` и `ECONNABORTED`. Остальные транспортные ошибки Node доезжают до
|
|
140
|
+
* лимитера под своим кодом (`ECONNRESET` при обрыве сокета, `ERR_BAD_RESPONSE` при
|
|
141
|
+
* 502/504 от шлюза), не находятся ни в жёстком, ни в мягком списке и потому считаются
|
|
142
|
+
* временными — то есть повторяются с backoff. Замерено на локальном сервере, рвущем
|
|
143
|
+
* соединение после приёма запроса: без hardErrorCodes портал выполняет crm.deal.add
|
|
144
|
+
* три раза, с ними — один. На SDK 2.0.0 был один: там транспорт маскировался в
|
|
145
|
+
* JSSDK_UNKNOWN_ERROR, а тот во встроенном жёстком списке.
|
|
146
|
+
*
|
|
147
|
+
* `ERR_NETWORK` и `ECONNABORTED` в списке не нужны: до лимитера они не доживают.
|
|
148
|
+
*
|
|
149
|
+
* Плата за список: `ECONNREFUSED`/`ENOTFOUND`/`EAI_AGAIN` доказывают, что запрос не ушёл,
|
|
150
|
+
* и SDK мог бы повторить их безопасно даже для создающих вызовов. Отказываемся сознательно —
|
|
151
|
+
* ровно так вёл себя 2.0.0, а один понятный инвариант дороже трёх сэкономленных попыток.
|
|
152
|
+
*/
|
|
153
|
+
const SDK_RESTRICTION_PARAMS = {
|
|
154
|
+
retryOnNetworkError: false,
|
|
155
|
+
hardErrorCodes: [
|
|
156
|
+
"ECONNRESET",
|
|
157
|
+
"ECONNREFUSED",
|
|
158
|
+
"ENOTFOUND",
|
|
159
|
+
"ETIMEDOUT",
|
|
160
|
+
"EHOSTUNREACH",
|
|
161
|
+
"ENETUNREACH",
|
|
162
|
+
"EAI_AGAIN",
|
|
163
|
+
"EPIPE",
|
|
164
|
+
"EPROTO",
|
|
165
|
+
"ERR_BAD_RESPONSE",
|
|
166
|
+
],
|
|
167
|
+
} as const satisfies RestrictionParams;
|
|
168
|
+
|
|
125
169
|
const confAuthB24 = new Conf({
|
|
126
170
|
cwd: path.resolve(CONFIG_DIR),
|
|
127
171
|
configName: "authB24",
|
|
@@ -173,10 +217,12 @@ interface V2Envelope {
|
|
|
173
217
|
* Читает сырой конверт ответа из AjaxResult.
|
|
174
218
|
*
|
|
175
219
|
* `getData()` отдаёт замороженную пару `{ result, time }` — полей `next` и `total`
|
|
176
|
-
* в ней нет намеренно (в restApi:v3 их не существует)
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
220
|
+
* в ней нет намеренно (в restApi:v3 их не существует). С версии 2.2.0 SDK снял
|
|
221
|
+
* `isMore()`/`getTotal()`/`getNext()` с удаления и объявил их постоянными читателями
|
|
222
|
+
* конверта restApi:v2, но числового смещения среди них так и нет: `isMore()` отвечает
|
|
223
|
+
* только «есть ли ещё», а `getNext(http)` сам делает следующий запрос, мимо нашего
|
|
224
|
+
* прогресса и retry. Поэтому конверт читаем напрямую: `_data` объявлен `protected`,
|
|
225
|
+
* а не приватным полем класса, поэтому в рантайме доступен.
|
|
180
226
|
*
|
|
181
227
|
* Это единственная точка связи с внутренностями SDK. Если она перестанет работать,
|
|
182
228
|
* она обязана упасть громко: тихо оборванная на первой странице выборка — потеря данных.
|
|
@@ -238,11 +284,22 @@ class B24SdkLogHandler implements Handler {
|
|
|
238
284
|
// нормальный путь через getResultData(): проверка существования в цикле
|
|
239
285
|
// дала бы строку на итерацию. Такие сообщения — уровень debug.
|
|
240
286
|
//
|
|
287
|
+
// 408 и 429 из правила исключены на будущее, сегодня эта ветка недостижима:
|
|
288
|
+
// status попадает в контекст записи уровня WARNING и выше ровно в одном месте
|
|
289
|
+
// SDK — #logNonRetryableClientError, а он вызывается из-под условия, которое
|
|
290
|
+
// 408 и 429 уже исключает. Сообщения лимитера про 429/503 идут другим путём,
|
|
291
|
+
// без status в контексте, и маппятся в warn независимо от этой строки.
|
|
292
|
+
// Условие оставлено потому, что фейлит в безопасную сторону: начни SDK класть
|
|
293
|
+
// status в такие записи — таймаут и упор в лимит будут видны, а не утонут в debug.
|
|
294
|
+
//
|
|
241
295
|
// Остальное: уровень ERROR у SDK — это диагностика отдельной неуспешной
|
|
242
|
-
//
|
|
296
|
+
// попытки. На транспортном сбое их пять — по числу наших попыток, повторы SDK
|
|
297
|
+
// выключены (см. SDK_RESTRICTION_PARAMS). На 429/503 и неизвестных 5xx SDK
|
|
298
|
+
// повторяет по-прежнему, там до пятнадцати: три внутри каждой из наших пяти.
|
|
243
299
|
// Уровень error пакета уходит в чат B24, поэтому маппим SDK-ERROR в warn:
|
|
244
|
-
// окончательный провал вызова логирует retry-слой,
|
|
245
|
-
const
|
|
300
|
+
// окончательный провал вызова логирует retry-слой, ровно один раз.
|
|
301
|
+
const isQuietClientError = status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
302
|
+
const level = isQuietClientError ? "debug" : record.level >= LogLevel.CRITICAL ? "error" : "warn";
|
|
246
303
|
|
|
247
304
|
// Контекст SDK (requestId, method, code, wait, status) уже прогнан
|
|
248
305
|
// через redactSensitiveParams — токены в него не попадают
|
|
@@ -303,7 +360,13 @@ function createB24Instance(): B24OAuth | null {
|
|
|
303
360
|
|
|
304
361
|
const secret: B24OAuthSecret = { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET };
|
|
305
362
|
|
|
306
|
-
|
|
363
|
+
// Параметры ограничителя задаём третьим аргументом конструктора: SDK сливает их
|
|
364
|
+
// с дефолтами (`{ ...ParamsFactory.getDefault(), ...restrictionParams }` в AbstractHttp)
|
|
365
|
+
// и передаёт обоим http-клиентам, v2 и v3, ещё до того как экземпляр можно использовать.
|
|
366
|
+
// Через setRestrictionManagerParams() было бы окно между созданием и настройкой,
|
|
367
|
+
// а результат вызова к тому же непроверяем: он завершается Promise.allSettled и
|
|
368
|
+
// не отклоняется никогда.
|
|
369
|
+
const b24 = new B24OAuth(authParams, secret, { restrictionParams: SDK_RESTRICTION_PARAMS });
|
|
307
370
|
|
|
308
371
|
// Диагностика SDK от WARNING и выше уходит в logs пакета: предупреждения
|
|
309
372
|
// callList/fetchList про игнорируемый order и остановку пагинации, сообщения лимитера.
|
|
@@ -491,9 +554,12 @@ export function stopProactiveRefresh(): void {
|
|
|
491
554
|
/**
|
|
492
555
|
* Проверяет, является ли ошибка из SDK сетевой.
|
|
493
556
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
557
|
+
* С версии 2.2.0 SDK отдаёт транспортный сбой честным кодом: NETWORK_ERROR (status 0)
|
|
558
|
+
* или REQUEST_TIMEOUT (status 408). До 2.2.0 он их маскировал — в логирующей ветке падал
|
|
559
|
+
* собственный TypeError, и наружу приходил AjaxError с кодом JSSDK_UNKNOWN_ERROR и status 0.
|
|
560
|
+
* Проверку по status 0 оставляем: она ловит всё, что SDK отдаёт без внятного кода.
|
|
561
|
+
* Сюда же попадает провал обновления токена внутри callMethod: SDK перезаворачивает
|
|
562
|
+
* SdkError в AjaxError с кодом JSSDK_UNKNOWN_ERROR и status 0, и исходный код теряется.
|
|
497
563
|
* RefreshTokenError приходит как SdkError с кодом вроде ENOTFOUND и без originalError.
|
|
498
564
|
*/
|
|
499
565
|
function isB24NetworkError(error: unknown): boolean {
|
|
@@ -511,6 +577,18 @@ function isB24NetworkError(error: unknown): boolean {
|
|
|
511
577
|
if (originalError && isNetworkError(originalError)) return true;
|
|
512
578
|
if (originalCode && NETWORK_ERROR_CODES.has(originalCode)) return true;
|
|
513
579
|
if (status === 0) return true;
|
|
580
|
+
|
|
581
|
+
// 502 Bad Gateway: шлюз перед порталом не смог получить ответ от upstream —
|
|
582
|
+
// запрос почти наверняка не выполнялся, повтор осмыслен. Мы держим этот код
|
|
583
|
+
// в SDK_RESTRICTION_PARAMS.hardErrorCodes, чтобы SDK его не повторял, но
|
|
584
|
+
// повторить его должен наш слой: там создающие вызовы отсекает гейт.
|
|
585
|
+
//
|
|
586
|
+
// 504 сюда намеренно не входит, хотя приходит тем же кодом ERR_BAD_RESPONSE:
|
|
587
|
+
// это «портал взял запрос и считает прямо сейчас, шлюз устал ждать». Повтор
|
|
588
|
+
// создающего вызова дал бы дубликат, а читающего — ничего: за таймаут шлюза
|
|
589
|
+
// запрос не уложился один раз, не уложится и на второй, зато пять тяжёлых
|
|
590
|
+
// повторов добавят нагрузки порталу ровно тогда, когда ему и так плохо.
|
|
591
|
+
if (status === 502) return true;
|
|
514
592
|
}
|
|
515
593
|
|
|
516
594
|
return isNetworkError(error);
|
|
@@ -695,8 +773,10 @@ function createFacade(target: B24OAuth): Record<string, (...args: any[]) => any>
|
|
|
695
773
|
const callMethod = async <T = any>(method: string, params?: object, start?: number): Promise<AjaxResult<T>> => {
|
|
696
774
|
const merged: TypeCallParams = { ...params };
|
|
697
775
|
|
|
698
|
-
// Явный params.start приоритетнее аргумента start — как в AbstractB24.callMethod
|
|
699
|
-
|
|
776
|
+
// Явный params.start приоритетнее аргумента start — как в AbstractB24.callMethod.
|
|
777
|
+
// typeof, а не только Number.isInteger: последний ничего не сужает, и при
|
|
778
|
+
// exactOptionalPropertyTypes у потребителя присваивание number | undefined не проходит
|
|
779
|
+
if (!("start" in merged && Number.isInteger(merged.start)) && typeof start === "number" && Number.isInteger(start)) {
|
|
700
780
|
merged.start = start;
|
|
701
781
|
}
|
|
702
782
|
|
|
@@ -790,11 +870,14 @@ function createFacade(target: B24OAuth): Record<string, (...args: any[]) => any>
|
|
|
790
870
|
};
|
|
791
871
|
|
|
792
872
|
async function* fetchListMethod(method: string, params?: any, idKey?: string, customKeyForResult?: string | null): AsyncGenerator<any[]> {
|
|
873
|
+
// Ключи со значением undefined не подставляем вовсе: при exactOptionalPropertyTypes
|
|
874
|
+
// у потребителя явный undefined не подходит опциональному полю опций SDK.
|
|
875
|
+
// Поведение прежнее — null и undefined одинаково означают «ключ не передан»
|
|
793
876
|
yield* v2().fetchList.make<any>({
|
|
794
877
|
method,
|
|
795
878
|
params,
|
|
796
|
-
idKey,
|
|
797
|
-
customKeyForResult
|
|
879
|
+
...(idKey === undefined ? {} : { idKey }),
|
|
880
|
+
...(customKeyForResult === null || customKeyForResult === undefined ? {} : { customKeyForResult }),
|
|
798
881
|
});
|
|
799
882
|
}
|
|
800
883
|
|
package/logs/logs.ts
CHANGED
|
@@ -30,6 +30,15 @@ const B24_CLIENT_ID = "6rf74zyu842h6op1z186b08k7aavqq7z";
|
|
|
30
30
|
/** Лимит длины сообщения для B24 чата (безопасный, с запасом на BB-обёртку) */
|
|
31
31
|
const B24_TEXT_LIMIT = 4000;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Бюджет времени на отправку error-лога в чат B24, мс.
|
|
35
|
+
*
|
|
36
|
+
* Своя константа, а не `FETCH_TIMEOUTS` из `utils/fetchRetry.ts`: тот модуль импортирует
|
|
37
|
+
* `logs` первой строкой, и обратный импорт дал бы цикл. Дублирование одного числа
|
|
38
|
+
* дешевле цикла зависимостей.
|
|
39
|
+
*/
|
|
40
|
+
const B24_CHAT_TIMEOUT_MS = 5_000;
|
|
41
|
+
|
|
33
42
|
// ==================== Настройки ====================
|
|
34
43
|
|
|
35
44
|
const configDir: string = process.env.CONFIG_DIR || "../config";
|
|
@@ -149,7 +158,10 @@ class LogsAPI {
|
|
|
149
158
|
|
|
150
159
|
/**
|
|
151
160
|
* Отправляет error-сообщение в чат B24 через webhook (fire-and-forget)
|
|
152
|
-
* Использует нативный fetch, а не fetchRetry — во избежание
|
|
161
|
+
* Использует нативный fetch, а не fetchRetry/fetchWithTimeout — во избежание
|
|
162
|
+
* циклической зависимости, поэтому и таймаут выставляется здесь вручную через
|
|
163
|
+
* `AbortSignal.timeout`: без него зависшее соединение никого бы не разбудило,
|
|
164
|
+
* вызов идёт без await
|
|
153
165
|
* Ошибки не пробрасываются, только console.error
|
|
154
166
|
*/
|
|
155
167
|
private sendToB24Chat(message: string, jsonData?: unknown): void {
|
|
@@ -195,6 +207,7 @@ class LogsAPI {
|
|
|
195
207
|
DIALOG_ID: this.b24ChatId,
|
|
196
208
|
MESSAGE: text,
|
|
197
209
|
}),
|
|
210
|
+
signal: AbortSignal.timeout(B24_CHAT_TIMEOUT_MS),
|
|
198
211
|
})
|
|
199
212
|
.then((response) => {
|
|
200
213
|
if (!response.ok) {
|
|
@@ -202,6 +215,11 @@ class LogsAPI {
|
|
|
202
215
|
}
|
|
203
216
|
})
|
|
204
217
|
.catch((error: unknown) => {
|
|
218
|
+
// Проверка локальная, без импорта isTimeoutError — тот модуль импортирует logs
|
|
219
|
+
if ((error as { name?: unknown })?.name === "TimeoutError") {
|
|
220
|
+
console.error(`Logs B24 Chat: не ответил за ${B24_CHAT_TIMEOUT_MS / 1000}с, сообщение не доставлено`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
205
223
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
206
224
|
console.error(`Logs B24 Chat: ошибка отправки — ${errMsg}`);
|
|
207
225
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrey4emk/npm-app-back-b24",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "Bitrix24 OAuth helpers for Node.js projects",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"url": "git+ssh://git@github.com/andrey4emk/npm_appBackB24.git"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.3.0"
|
|
23
|
+
},
|
|
21
24
|
"author": "andrey4emk",
|
|
22
25
|
"license": "MIT",
|
|
23
26
|
"bugs": {
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"utils/fetchRetry.ts"
|
|
38
41
|
],
|
|
39
42
|
"dependencies": {
|
|
40
|
-
"@bitrix24/b24jssdk": "2.
|
|
43
|
+
"@bitrix24/b24jssdk": "2.2.0",
|
|
41
44
|
"@types/express": "5.0.6",
|
|
42
45
|
"@types/luxon": "3.7.4",
|
|
43
46
|
"@types/node": "25.9.5",
|