@andrey4emk/npm-app-back-b24 3.8.0 → 3.8.2

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
@@ -474,6 +474,8 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
474
474
  processId: "12345",
475
475
  message: [
476
476
  {
477
+ messageId: "7cf5d29bee90cc37382a9fff07fc2a43", // MESSAGE_ID записи очереди — ключ для clear()
478
+ eventId: "4821", // ID записи очереди, информационное; в clear() не передавать
477
479
  connectorId: "...",
478
480
  lineId: "...",
479
481
  chatId: "...",
@@ -498,9 +500,11 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
498
500
  ```
499
501
 
500
502
  - **Особенности:**
501
- - Автоматически фильтрует и удаляет события от системного пользователя (`user_id: '138'`).
503
+ - **Автоматически отбрасывает и очищает события пользователя 138** — владельца OAuth-токенов приложений. Это защита от петли: всё, что приложения сами пишут в B24, приходит событием с `user_id: '138'`, и без фильтра код обрабатывал бы собственные записи повторно. Оборотная сторона: ручные правки под этой же учёткой (администратор портала) тоже не возвращаются. **Для полной выгрузки всех изменений класс не подходит** — там читать `event.offline.get` напрямую.
502
504
  - Для коннекторных событий конвертирует файлы в формат с типами `image`, `video`, `document`.
503
505
  - Если событий нет, возвращает `data` с пустыми массивами.
506
+ - **Гранулярность очистки — запись очереди, а не сообщение.** Одна запись коннектора (один `MESSAGE_ID`) может нести несколько сообщений, и они приходят отдельными элементами `message` с одинаковым `messageId`. Очистка по этому ключу удаляет запись целиком: если из двух сообщений одной записи ушло одно, `clear()` заберёт и неотправленное. Строить поэлементную очистку с точностью до сообщения на `messageId` нельзя. Для дедупликации отдельных сообщений он не годится по той же причине — у сообщений одной записи он общий, для этого есть `im.id`. Обратная сторона: запись, из которой не разобрано ни одного сообщения, в `data.message` не попадёт вовсе, и очистка по собранным `messageId` её не заберёт. Безопасный режим — `clear(processId)` целиком.
507
+ - Типы `ConnectorMessage`, `ConnectorEvents` и `EntityEvents` экспортируются из пакета.
504
508
 
505
509
  ```js
506
510
  const { error, data } = await event.get("ONCRMDYNAMICITEMUPDATE_149");
@@ -514,7 +518,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
514
518
  - **Параметры:**
515
519
 
516
520
  - `processId` (string) — ID процесса (получен из `get()`).
517
- - `messageId` (string|string[]) — опциональный ID или массив ID сообщений для очистки.
521
+ - `messageId` (string|string[]) — опциональный `MESSAGE_ID` или массив `MESSAGE_ID` записей очереди для очистки. Без него очищается весь пакет `processId`. Пустой массив означает «очищать нечего»: вызов до API не доходит и возвращает `error: false` (раньше уезжало `message_id: []`). «Мягкая» ошибка портала, например `ERROR_ENTITY_NOT_FOUND` на протухшем `processId`, возвращается как `error: true`.
518
522
 
519
523
  - **Возвращает:** промис с объектом `{ error, message, data }`.
520
524
 
@@ -3,7 +3,18 @@ import { $b24, getResultData, type B24MethodCaller } from "./b24.ts";
3
3
  // ==================== Типы ====================
4
4
 
5
5
  /** Сообщение из коннектора */
6
- interface ConnectorMessage {
6
+ export interface ConnectorMessage {
7
+ /**
8
+ * `MESSAGE_ID` записи очереди офлайн-событий — ключ для `clear()`.
9
+ * Идентифицирует запись, а не отдельное сообщение: у сообщений одной записи
10
+ * он общий, поэтому для дедупликации сообщений не годится (для этого `im.id`)
11
+ */
12
+ messageId: string;
13
+ /**
14
+ * `ID` записи очереди офлайн-событий. Поле информационное, для сверки
15
+ * с `event.offline.list`. В `clear()` не передавать — метод принимает `MESSAGE_ID`
16
+ */
17
+ eventId: string;
7
18
  connectorId: string;
8
19
  lineId: string;
9
20
  chatId: string;
@@ -14,7 +25,7 @@ interface ConnectorMessage {
14
25
  }
15
26
 
16
27
  /** События коннектора */
17
- interface ConnectorEvents {
28
+ export interface ConnectorEvents {
18
29
  processId: string | null;
19
30
  message: ConnectorMessage[];
20
31
  }
@@ -26,7 +37,7 @@ interface MessageEntityLink {
26
37
  }
27
38
 
28
39
  /** События сущностей (сделки, смарт-процессы) */
29
- interface EntityEvents {
40
+ export interface EntityEvents {
30
41
  processId: string | null;
31
42
  entitysId: string[];
32
43
  arrMessageIdAndEntityId: MessageEntityLink[];
@@ -41,6 +52,7 @@ interface StandardResult {
41
52
 
42
53
  /** Офлайн-событие Bitrix24 */
43
54
  interface OfflineEvent {
55
+ ID: string;
44
56
  MESSAGE_ID: string;
45
57
  EVENT_NAME: string;
46
58
  EVENT_DATA: any;
@@ -59,7 +71,19 @@ interface OfflineEventsResponse {
59
71
 
60
72
  // ==================== Константы ====================
61
73
 
62
- const SYSTEM_USER_ID = "138"; // ID системного пользователя для фильтрации
74
+ /**
75
+ * Пользователь, чьи события класс отбрасывает и сразу очищает из очереди.
76
+ *
77
+ * Это защита от петли, а не случайность: 138 — владелец OAuth-токенов приложений
78
+ * на портале, и всё, что приложения пишут в B24 (обновление сделки, комментарий,
79
+ * задача), приходит офлайн-событием с `user_id: "138"`. Без фильтра правка сделки
80
+ * кодом возвращалась бы событием в тот же код и обрабатывалась бы второй раз.
81
+ *
82
+ * Оборотная сторона: ручные правки под этой же учёткой (это администратор портала)
83
+ * тоже пропадают. Поэтому класс не годится для задач полной выгрузки или витрин,
84
+ * где нужны все изменения без исключения — там читать `event.offline.get` напрямую
85
+ */
86
+ const SYSTEM_USER_ID = "138";
63
87
 
64
88
  // ==================== Утилиты ====================
65
89
 
@@ -148,7 +172,24 @@ export class Event {
148
172
  }
149
173
 
150
174
  /**
151
- * Получает офлайн-события по имени
175
+ * Получает офлайн-события по имени.
176
+ *
177
+ * Гранулярность очистки — запись очереди, а не сообщение. Для коннектора одна
178
+ * запись (один `MESSAGE_ID`) может нести несколько сообщений, и `get()` раскладывает
179
+ * их через `flatMap` — у таких сообщений `messageId` совпадает. Очистка по этому
180
+ * ключу удалит запись целиком: если из двух сообщений одной записи ушло одно,
181
+ * `clear()` заберёт и неотправленное. Поэлементную очистку с точностью до
182
+ * сообщения на этом ключе построить нельзя.
183
+ *
184
+ * Обратная сторона: запись, из которой не разобрано ни одного сообщения
185
+ * (пустой или битый `MESSAGES`), в `data.message` не попадает вовсе — очистка
186
+ * по собранным `messageId` её не заберёт, и она останется зарезервированной
187
+ * под тем же `process_id`. Безопасный режим — `clear(processId)` целиком.
188
+ *
189
+ * События пользователя `SYSTEM_USER_ID` (138, владелец токенов приложений)
190
+ * отбрасываются и очищаются здесь же — защита от петли «приложение записало →
191
+ * событие → приложение обработало свою же запись». Для полной выгрузки всех
192
+ * изменений класс не подходит, см. комментарий у константы.
152
193
  */
153
194
  async get(eventName: string): Promise<StandardResult> {
154
195
  try {
@@ -184,6 +225,10 @@ export class Event {
184
225
  return normalizeMessages(eventData.MESSAGES)
185
226
  .filter((messageData) => messageData?.chat && messageData?.message)
186
227
  .map((messageData) => ({
228
+ // Портал отдаёт идентификаторы то строкой, то числом —
229
+ // приводим, чтобы тип `string` не врал
230
+ messageId: String(event.MESSAGE_ID ?? ""),
231
+ eventId: String(event.ID ?? ""),
187
232
  connectorId: eventData.CONNECTOR,
188
233
  lineId: eventData.LINE,
189
234
  chatId: messageData.chat.id,
@@ -210,6 +255,9 @@ export class Event {
210
255
 
211
256
  if (arrEventsToClear.length > 0) {
212
257
  const arrMessageIdToClear = arrEventsToClear.map((event) => event.MESSAGE_ID);
258
+ // Результат намеренно не проверяется: отказ здесь не фатален — те же
259
+ // записи придут следующим опросом, а бросок лишил бы потребителя всей
260
+ // выборки активных событий
213
261
  await this.b24.callMethod("event.offline.clear", {
214
262
  process_id: arrOfflineEvents.process_id,
215
263
  message_id: arrMessageIdToClear,
@@ -243,11 +291,29 @@ export class Event {
243
291
  */
244
292
  async clear(processId: string, messageId?: string | string[]): Promise<StandardResult> {
245
293
  try {
294
+ // Пустой массив в JavaScript истинен и уезжал в теле запроса как `message_id: []` —
295
+ // это не документированный режим «очистить весь пакет» (что портал делает с
296
+ // пустым списком, не проверялось). Считаем пустой список за «очищать нечего»
297
+ // и до API не доходим: молча сносить весь пакет по пустому списку опасно —
298
+ // зарезервированный пакет портал повторно не выдаёт
299
+ if (Array.isArray(messageId) && messageId.length === 0) {
300
+ return { error: false, message: "Список записей пуст, очищать нечего", data: null };
301
+ }
302
+
246
303
  const params: any = { process_id: processId };
247
304
  if (messageId) {
248
305
  params.message_id = messageId;
249
306
  }
250
- await this.b24.callMethod("event.offline.clear", params);
307
+ const response = await this.b24.callMethod("event.offline.clear", params);
308
+ // «Мягкую» ошибку (например, ERROR_ENTITY_NOT_FOUND на протухшем process_id)
309
+ // SDK не бросает, а возвращает AjaxResult с isSuccess: false. Без проверки
310
+ // метод рапортовал бы «События очищены» при неочищенной очереди.
311
+ // Именно isSuccess, а не getResultData(): та бросает ещё и на result == null,
312
+ // а что портал кладёт в result этого метода, не проверено
313
+ if (!response.isSuccess) {
314
+ const messages = response.getErrorMessages().join("; ") || "неизвестная ошибка";
315
+ throw new Error(`event.offline.clear: Bitrix24 вернул ошибку — ${messages}`);
316
+ }
251
317
 
252
318
  return { error: false, message: "События очищены", data: null };
253
319
  } catch (error: any) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrey4emk/npm-app-back-b24",
3
- "version": "3.8.0",
3
+ "version": "3.8.2",
4
4
  "description": "Bitrix24 OAuth helpers for Node.js projects",
5
5
  "main": "index.ts",
6
6
  "type": "module",