bx-rest 1.5.0 → 1.6.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.
Files changed (181) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.en.md +116 -5
  3. package/README.md +117 -5
  4. package/esm2022/functions/errors.mjs +17 -5
  5. package/esm2022/functions/mapResult.mjs +4 -2
  6. package/esm2022/functions/uniqueElementCode.mjs +24 -0
  7. package/esm2022/map/disk.mjs +13 -0
  8. package/esm2022/map/im/recent.mjs +1 -9
  9. package/esm2022/map/task.mjs +11 -5
  10. package/esm2022/map/tasks/task.mjs +3 -3
  11. package/esm2022/map/tasks.mjs +4 -3
  12. package/esm2022/map/user.mjs +5 -4
  13. package/esm2022/public-api.mjs +2 -2
  14. package/esm2022/request-navvy/bizproc.mjs +19 -10
  15. package/esm2022/request-navvy/calendar/event.mjs +1 -9
  16. package/esm2022/request-navvy/disk/operation.mjs +3 -10
  17. package/esm2022/request-navvy/pull/config.mjs +1 -3
  18. package/esm2022/request-navvy/socialnetwork/api/livefeed.mjs +1 -5
  19. package/esm2022/request-navvy/task/operation/elapseditem.mjs +14 -71
  20. package/esm2022/request-navvy/tasks/task.mjs +1 -119
  21. package/esm2022/rest/map.mjs +3 -3
  22. package/esm2022/rest/navvy.mjs +66 -64
  23. package/esm2022/services/base.mjs +7 -7
  24. package/esm2022/services/context.mjs +59 -0
  25. package/esm2022/services/http/HttpBX.mjs +55 -20
  26. package/esm2022/services/http/authRefresh.mjs +25 -23
  27. package/esm2022/services/http/http.mjs +14 -69
  28. package/esm2022/services/http/middleware.mjs +149 -0
  29. package/esm2022/services/http/sessionKey.mjs +16 -5
  30. package/esm2022/services/http/transport.mjs +5 -0
  31. package/esm2022/services/navvy/extends/navvy-pag-base.mjs +63 -9
  32. package/esm2022/services/navvy/navvy-alternative-pagination-navigation.mjs +21 -71
  33. package/esm2022/services/navvy/navvy-pag-nav-result-key-object.mjs +10 -49
  34. package/esm2022/services/navvy/navvy-pag-nav-result-key.mjs +14 -73
  35. package/esm2022/services/navvy/navvy-pag-nav-tasks.mjs +4 -76
  36. package/esm2022/services/navvy/navvy-pag-nav-with-useless-key.mjs +10 -76
  37. package/esm2022/services/navvy/navvy-pag.mjs +7 -65
  38. package/esm2022/services/navvy/navvy-support.mjs +2 -15
  39. package/esm2022/services/navvy.mjs +18 -31
  40. package/esm2022/typification/rest/calendar/event/get.mjs +1 -1
  41. package/esm2022/typification/rest/settings.mjs +1 -1
  42. package/esm2022/typification/rest/tasks/task/get.mjs +1 -1
  43. package/esm2022/typification/rest/user/user.mjs +1 -1
  44. package/esm2022/typification/settings.mjs +1 -1
  45. package/fesm2022/bx-rest.mjs +860 -2038
  46. package/fesm2022/bx-rest.mjs.map +1 -1
  47. package/functions/errors.d.ts +9 -0
  48. package/functions/uniqueElementCode.d.ts +12 -0
  49. package/map/disk.d.ts +10 -0
  50. package/map/task.d.ts +3 -3
  51. package/map/tasks/task.d.ts +1 -1
  52. package/map/tasks.d.ts +1 -1
  53. package/package.json +5 -2
  54. package/public-api.d.ts +7 -3
  55. package/request-navvy/pull/config.d.ts +0 -2
  56. package/rest/map.d.ts +2 -2
  57. package/rest/navvy.d.ts +9 -0
  58. package/services/context.d.ts +43 -0
  59. package/services/http/HttpBX.d.ts +10 -0
  60. package/services/http/authRefresh.d.ts +5 -1
  61. package/services/http/http.d.ts +12 -9
  62. package/services/http/middleware.d.ts +31 -0
  63. package/services/http/sessionKey.d.ts +10 -0
  64. package/services/http/transport.d.ts +28 -0
  65. package/services/navvy/extends/navvy-pag-base.d.ts +16 -2
  66. package/services/navvy/navvy-alternative-pagination-navigation.d.ts +5 -2
  67. package/services/navvy/navvy-pag-nav-result-key-object.d.ts +0 -2
  68. package/services/navvy/navvy-pag-nav-result-key.d.ts +1 -4
  69. package/services/navvy/navvy-pag-nav-tasks.d.ts +2 -9
  70. package/services/navvy/navvy-pag-nav-with-useless-key.d.ts +0 -2
  71. package/services/navvy/navvy-pag.d.ts +0 -2
  72. package/services/navvy/navvy-support.d.ts +0 -10
  73. package/services/navvy.d.ts +1 -0
  74. package/typification/rest/calendar/event/get.d.ts +1 -1
  75. package/typification/rest/settings.d.ts +1 -0
  76. package/typification/rest/user/user.d.ts +3 -3
  77. package/typification/settings.d.ts +34 -0
  78. package/esm2022/functions/errorHandler.mjs +0 -34
  79. package/esm2022/request/app.mjs +0 -13
  80. package/esm2022/request/bizproc/activity.mjs +0 -3
  81. package/esm2022/request/bizproc/event.mjs +0 -3
  82. package/esm2022/request/bizproc/robot.mjs +0 -3
  83. package/esm2022/request/bizproc/task.mjs +0 -3
  84. package/esm2022/request/bizproc/workflow/instance.mjs +0 -3
  85. package/esm2022/request/bizproc/workflow/template.mjs +0 -3
  86. package/esm2022/request/bizproc/workflow.mjs +0 -18
  87. package/esm2022/request/bizproc.mjs +0 -15
  88. package/esm2022/request/department.mjs +0 -26
  89. package/esm2022/request/disk/attachedObject.mjs +0 -14
  90. package/esm2022/request/disk/file.mjs +0 -18
  91. package/esm2022/request/disk/folder.mjs +0 -117
  92. package/esm2022/request/disk.mjs +0 -11
  93. package/esm2022/request/lists/element.mjs +0 -18
  94. package/esm2022/request/lists/field.mjs +0 -35
  95. package/esm2022/request/lists.mjs +0 -86
  96. package/esm2022/request/log/blogcomment.mjs +0 -3
  97. package/esm2022/request/log/blogpost.mjs +0 -18
  98. package/esm2022/request/log.mjs +0 -9
  99. package/esm2022/request/pull/application/config.mjs +0 -11
  100. package/esm2022/request/pull/channel/public.mjs +0 -18
  101. package/esm2022/request/pull/config.mjs +0 -13
  102. package/esm2022/request/pull.mjs +0 -11
  103. package/esm2022/request/server.mjs +0 -14
  104. package/esm2022/request/socialnetwork/api/contentview.mjs +0 -14
  105. package/esm2022/request/socialnetwork/api/livefeed/blogpost/important.mjs +0 -14
  106. package/esm2022/request/socialnetwork/api/livefeed/blogpost.mjs +0 -7
  107. package/esm2022/request/socialnetwork/api/livefeed.mjs +0 -7
  108. package/esm2022/request/socialnetwork/api.mjs +0 -9
  109. package/esm2022/request/socialnetwork.mjs +0 -8
  110. package/esm2022/request/sonet_group.mjs +0 -24
  111. package/esm2022/request/task/commentitem.mjs +0 -32
  112. package/esm2022/request/task/elapseditem.mjs +0 -40
  113. package/esm2022/request/task/item/userfield.mjs +0 -14
  114. package/esm2022/request/task/item.mjs +0 -7
  115. package/esm2022/request/task/planner.mjs +0 -11
  116. package/esm2022/request/task.mjs +0 -13
  117. package/esm2022/request/tasks/task/result.mjs +0 -22
  118. package/esm2022/request/tasks/task.mjs +0 -110
  119. package/esm2022/request/tasks.mjs +0 -7
  120. package/esm2022/request/user/userfield.mjs +0 -14
  121. package/esm2022/request/user.mjs +0 -40
  122. package/esm2022/rest/base.mjs +0 -39
  123. package/esm2022/services/http/HttpData.mjs +0 -77
  124. package/esm2022/services/http/base/base.mjs +0 -4
  125. package/esm2022/services/http/httpNavvy.mjs +0 -14
  126. package/esm2022/typification/base/answers.mjs +0 -438
  127. package/esm2022/typification/base/params.mjs +0 -525
  128. package/esm2022/typification/base/workpiece/methods-list.mjs +0 -2
  129. package/esm2022/typification/base/workpiece/methods.mjs +0 -2
  130. package/functions/errorHandler.d.ts +0 -10
  131. package/request/app.d.ts +0 -8
  132. package/request/bizproc/activity.d.ts +0 -2
  133. package/request/bizproc/event.d.ts +0 -2
  134. package/request/bizproc/robot.d.ts +0 -2
  135. package/request/bizproc/task.d.ts +0 -2
  136. package/request/bizproc/workflow/instance.d.ts +0 -2
  137. package/request/bizproc/workflow/template.d.ts +0 -2
  138. package/request/bizproc/workflow.d.ts +0 -13
  139. package/request/bizproc.d.ts +0 -12
  140. package/request/department.d.ts +0 -18
  141. package/request/disk/attachedObject.d.ts +0 -8
  142. package/request/disk/file.d.ts +0 -10
  143. package/request/disk/folder.d.ts +0 -96
  144. package/request/disk.d.ts +0 -8
  145. package/request/lists/element.d.ts +0 -11
  146. package/request/lists/field.d.ts +0 -11
  147. package/request/lists.d.ts +0 -31
  148. package/request/log/blogcomment.d.ts +0 -2
  149. package/request/log/blogpost.d.ts +0 -11
  150. package/request/log.d.ts +0 -6
  151. package/request/pull/application/config.d.ts +0 -5
  152. package/request/pull/channel/public.d.ts +0 -5
  153. package/request/pull/config.d.ts +0 -7
  154. package/request/pull.d.ts +0 -8
  155. package/request/server.d.ts +0 -7
  156. package/request/socialnetwork/api/contentview.d.ts +0 -6
  157. package/request/socialnetwork/api/livefeed/blogpost/important.d.ts +0 -6
  158. package/request/socialnetwork/api/livefeed/blogpost.d.ts +0 -4
  159. package/request/socialnetwork/api/livefeed.d.ts +0 -4
  160. package/request/socialnetwork/api.d.ts +0 -6
  161. package/request/socialnetwork.d.ts +0 -4
  162. package/request/sonet_group.d.ts +0 -8
  163. package/request/task/commentitem.d.ts +0 -23
  164. package/request/task/elapseditem.d.ts +0 -26
  165. package/request/task/item/userfield.d.ts +0 -6
  166. package/request/task/item.d.ts +0 -4
  167. package/request/task/planner.d.ts +0 -4
  168. package/request/task.d.ts +0 -10
  169. package/request/tasks/task/result.d.ts +0 -13
  170. package/request/tasks/task.d.ts +0 -37
  171. package/request/tasks.d.ts +0 -4
  172. package/request/user/userfield.d.ts +0 -8
  173. package/request/user.d.ts +0 -24
  174. package/rest/base.d.ts +0 -33
  175. package/services/http/HttpData.d.ts +0 -7
  176. package/services/http/base/base.d.ts +0 -7
  177. package/services/http/httpNavvy.d.ts +0 -10
  178. package/typification/base/answers.d.ts +0 -12
  179. package/typification/base/params.d.ts +0 -16
  180. package/typification/base/workpiece/methods-list.d.ts +0 -247
  181. package/typification/base/workpiece/methods.d.ts +0 -20
package/CHANGELOG.md CHANGED
@@ -7,6 +7,183 @@
7
7
  История до версии 1.0.0 здесь не восстанавливалась — смотрите `git log` для
8
8
  более ранних изменений.
9
9
 
10
+ ## [1.6.0] - 2026-11-10
11
+
12
+ ### Added
13
+
14
+ - Клиент со своими настройками: `new BXRestNavvy(settings)`. Все запросы
15
+ такого клиента идут на его портал с его токеном, продление авторизации у
16
+ него своё, а `BXRestSettings` на него не влияют. Так в одном процессе можно
17
+ работать с несколькими порталами. `new BXRestNavvy()` без аргумента
18
+ работает с глобальными `BXRestSettings`, как раньше. Подробнее — в разделе
19
+ README «Несколько порталов».
20
+ - Настройки `retryOnQueryLimit`, `timeout` и `throttle` — в `BXRestSettings`
21
+ или у клиента `new BXRestNavvy(settings)`. `retryOnQueryLimit` повторяет
22
+ запрос при `QUERY_LIMIT_EXCEEDED` с растущей паузой, `timeout` отменяет
23
+ запрос, который идёт дольше заданного времени, с ошибкой
24
+ `BXRestTimeoutError`, `throttle` пропускает не больше N запросов в секунду.
25
+ `true` включает значения по умолчанию: 3 попытки с паузой от 1000 мс,
26
+ 60 000 мс и 2 запроса в секунду. Пока настройки не заданы, запросы идут как
27
+ раньше. Подробнее — в разделе README «Повтор, таймаут и очередь запросов».
28
+ - Настройка `middleware` — свои функции, через которые проходит каждый
29
+ запрос: логирование, подмена заголовков, свой транспорт вместо axios. Типы
30
+ `BXRestMiddleware`, `BXRestRequestConfig` и `BXRestSend` экспортируются из
31
+ `bx-rest`. Подробнее — в разделе README «Свои middleware».
32
+
33
+ ### Changed
34
+
35
+ - Отписка до ответа теперь отменяет HTTP-запрос, как у `HttpClient` в
36
+ Angular. Раньше запрос доходил до конца, а ответ отбрасывался. На
37
+ `firstValueFrom` и обычную подписку это не влияет: они отписываются после
38
+ ответа. Отменяются запросы, которые обрывают `switchMap`, `takeUntil`,
39
+ `timeout` из RxJS или явный `unsubscribe()`. Отмена не откатывает то, что
40
+ портал уже успел сделать.
41
+ - Один экземпляр axios на клиента: он создаётся только при первом запросе.
42
+ Создание объекта запроса стало примерно в 100 раз быстрее (10,7 → 0,15 мкс),
43
+ при `new BXRestNavvy()` — вдвое.
44
+ - Ошибки HTTP библиотека узнаёт по полю `response: { status, data }`, а не
45
+ через `axios.isAxiosError`. Для ошибок axios ничего не меняется, а
46
+ `isBXRestAuthError` и `BXRestApiError` теперь работают и с ошибками своего
47
+ транспорта такой же формы.
48
+ - `bizProc.startProcFormNewsFeed()`: если портал вернул ошибку Bitrix при
49
+ создании элемента списка в ответе 2xx, вызов завершается ошибкой
50
+ `BXRestApiError` с кодом и описанием ошибки. Раньше в этом случае приходила
51
+ общая ошибка `Не удалось запустить бизнес процесс`. Ошибки со статусом не 2xx
52
+ и раньше приходили как `BXRestApiError`.
53
+ - Ошибки, которые библиотека создаёт сама, теперь на английском, как и
54
+ остальные её сообщения (`SessionKeyError`, `BXRestTimeoutError`, ошибки
55
+ разбора значений). Это ошибки `bizProc.startProcFormNewsFeed()`,
56
+ `disk.operation.*` и `task.elapsedItem.operation.*`. Код, который
57
+ сравнивает `err.message` с русским текстом, нужно обновить. Ошибки Bitrix
58
+ (`BXRestApiError`) приходят с текстом портала, как и раньше.
59
+
60
+ ### Removed
61
+
62
+ - У `BXRestRequest` удалены публичные члены, которые библиотека не
63
+ использовала: `defSettings`, `getHttpParamsPost()`, `sliceArrayIntoChunks()`
64
+ и `removingMultipleSlashes()`. Они достались от внутреннего базового класса
65
+ и запросы не отправляли. `post()`, `get()`, `httpPost()` и `httpGet()` не
66
+ изменились. Код, который обращался к удалённым членам, перестанет
67
+ компилироваться.
68
+ - Из `bx-rest/services/navvy` удалён абстрактный класс `NavvySupport`:
69
+ библиотека его не использовала. Тип `ReturnTypeNavvy` остался.
70
+ - Из `bx-rest` удалён класс `HttpNavvyServices` вместе с его типами
71
+ `iBXRestParams` и `iBXRestAnswers`. Он отправлял запросы с типами для
72
+ небольшого набора методов, сама библиотека его не использовала, а в README
73
+ он не описан. Вместо него используйте `BXRestNavvy` или
74
+ `BXRestRequest.post()`.
75
+ - Из исходников удалены 63 модуля, недостижимых из точек входа пакета (старый
76
+ слой `request/*`, `rest/base`, `functions/errorHandler`, `isDate`, `isEqual`,
77
+ `isValidDate`, `serializeJavascript`, `services/permission` и другие), и
78
+ закомментированный код. Файлы 52 из них (`.d.ts` и `esm2022/*.mjs`) лежали в
79
+ пакете, но ни основной бандл, ни `exports` их не подключали, поэтому через
80
+ `import` из `bx-rest` и его подпутей они были недоступны. Из пакета удалено
81
+ 104 файла. Затронуть это может только код, который импортировал типы по
82
+ внутреннему пути вроде `bx-rest/request/...` при `moduleResolution: "node"`.
83
+
84
+ ### Fixed
85
+
86
+ - Проверка типов под `moduleResolution: "nodenext"` или `"node16"` с
87
+ `skipLibCheck: false` больше не падает с ошибкой TS1479 в
88
+ `typification/rest/calendar/event/get.d.ts`. Типы `countries-and-timezones`
89
+ теперь подключаются через `resolution-mode: 'import'`: ветка `require` этого
90
+ пакета указывает на несуществующий файл типов. Сами типы не изменились, код
91
+ пакета тоже. В режиме `bundler` (Angular, Vite, webpack) ошибки не было.
92
+ - `bizProc.startProcFormNewsFeed()` создаёт `ELEMENT_CODE` из префикса
93
+ `bx_rest_` и 128 случайных бит (`crypto.getRandomValues`). Раньше код
94
+ собирался из префикса другого приложения `trace_studio_`, времени и
95
+ `Math.random()`. Если портал отвечает, что такой код уже занят
96
+ (`ERROR_ELEMENT_ALREADY_EXISTS`), вызов создаёт новый код и повторяет
97
+ запрос, всего до трёх попыток.
98
+ - `disk.operation.getOfCreateRootFolderApp()` и
99
+ `getOfCreateRootContentFolderApp()`: если не удалось получить хранилище
100
+ приложения, ошибка теперь так и называется — `Could not get the app
101
+ storage`. Раньше в ней был чужой текст про учёт времени: `Отсутствуют права
102
+ добавление записи затраченного времени`.
103
+
104
+ ## [1.5.1] - 2026-11-05
105
+
106
+ ### Added
107
+
108
+ - Типы `iBXRestSettings` и `BXRestApiErrorDetails` экспортируются из `bx-rest`,
109
+ а `iBXRestSettings` — ещё и из `bx-rest/typification/settings`, где его раньше
110
+ не было. Новый подпуть `bx-rest/typification/batch` — типы `batch`.
111
+
112
+ ### Changed
113
+
114
+ - `.resAll()` теперь завершается ошибкой `BXRestApiError`, если любая страница
115
+ внутри batch-запроса вернула ошибку (`result_error`) или сам batch-запрос
116
+ вернул ошибку в теле ответа 2xx. Раньше такие страницы молча пропускались, и
117
+ вызывающий код получал неполный список как полный. Код, который рассчитывал
118
+ на частичный результат, должен обрабатывать ошибку.
119
+ - `iBXRestUser.DATE_REGISTER` и `iBXRestUser.PERSONAL_BIRTHDAY` стали
120
+ необязательными: см. исправление `user.get` ниже. Код, который обращается к
121
+ этим полям без проверки, перестанет компилироваться в режиме `strict`.
122
+ - Отписка от `.resAll()` останавливает запрос оставшихся страниц. Уже
123
+ отправленный HTTP-запрос не отменяется. `.resAll()` по-прежнему кэширует
124
+ завершённый результат там, где делал это раньше.
125
+ - `.resAll()` у всех видов пагинации отправляет batch-запросы по 50 команд.
126
+ Раньше `pagNavResultKeyObject` отправлял отдельный batch на каждую страницу.
127
+
128
+ ### Fixed
129
+
130
+ - `.resAll()` у `pagNavWithUselessKey` (`lists.field.get`) складывал страницы
131
+ после первой под числовые ключи вместо объединения полей, а без своего `map`
132
+ накапливал результат между запусками одного и того же объекта запроса.
133
+ - `.resAll()` падал с `RangeError: Maximum call stack size exceeded` на списках
134
+ примерно от 124 тысяч записей.
135
+ - `.resAll()` у `pagNav` при нескольких одновременных подписчиках мог отдавать
136
+ записи по два раза: подписчики делили один массив результата.
137
+ - `.resAll()` учитывает параметры запроса: при заданном `start` страница больше
138
+ не запрашивается повторно, а `alterPagNav` при `nPageSize` меньше 50 загружает
139
+ все страницы — раньше он отдавал только часть записей.
140
+ - `.resAll()` у `pagNavTasks`, `pagNavWithUselessKey` и `alterPagNav` не
141
+ запрашивает лишнюю пустую страницу, если число записей кратно размеру страницы.
142
+ - Ответ 2xx не от Bitrix (например, HTML-страница входа от прокси) больше не даёт
143
+ `TypeError: Cannot use 'in' operator...`: `.res()` и `.resAll()` завершаются
144
+ обычным `Error` с названием метода. Пустое тело `.res()` по-прежнему отдаёт как
145
+ `undefined`, а `.resAll()` завершается той же ошибкой.
146
+ - `isBXRestAnswerSuccess()` и `BXRestMapResult()` падали с `TypeError`, если им
147
+ передавали не объект.
148
+ - `BXRestRequest.get()`:
149
+ - вложенные параметры уходили как `filter=[object Object]`, а массивы — как
150
+ `select=ID,TITLE`. Теперь они сериализуются так же, как в POST;
151
+ - при `auth.key: 'OAuth2'` токен попадал в адрес запроса как `OAuth2=...`.
152
+ Теперь он не отправляется, как и в POST.
153
+ - `BXRestSettings.withCredentials` не действовал на запросы `.get()` и на
154
+ POST-запросы при `auth.key: 'sessid'`.
155
+ - `toDate()` превращал несуществующие даты в соседние (`31.02.2026` — в 3 марта).
156
+ Теперь это ошибка `Wrong date format`, как и у других неверных дат.
157
+ - `user.get`, `user.current` и другие методы с тем же маппером: пустой
158
+ `PERSONAL_BIRTHDAY` давал `Invalid Date`, а ответ без `DATE_REGISTER` или
159
+ `PERSONAL_BIRTHDAY` завершал весь запрос ошибкой. Теперь в этих случаях поле
160
+ равно `undefined`.
161
+ - `bizProc.startProcFormNewsFeed()` заменял текущей датой любую переданную строку
162
+ `DATE_CREATE` и падал с `TypeError`, если передать `Date`.
163
+ - `task.elapsedItem.operation.getListByIDTask([])` завершался без значения, и
164
+ `firstValueFrom` падал с `EmptyError`. Теперь пустой список задач даёт `[]` без
165
+ запроса. В ошибке `operation.add()` исправлен текст: «Отсутствуют права на
166
+ добавление записи затраченного времени».
167
+ - `new BXRestMap().disk` возвращал класс запросов вместо маппера, а
168
+ `.task.commentItem`, `.task.elapsedItem`, `.task.stages`, `.tasks.task` и
169
+ `.tasks.task.result` были недоступны.
170
+ - Импорт одного класса больше не тянет в бандл всю библиотеку: `BXRestFrame` без
171
+ остальной библиотеки весит около 8 КБ вместо 117 КБ. Мешали статический
172
+ инициализатор и декораторы, которые выполнялись при загрузке модуля. В
173
+ результате убрано статическое поле `BXRestNavvyPullConfig.public` — оно нигде не
174
+ использовалось, а сам класс не экспортируется.
175
+
176
+ ### Security
177
+
178
+ - `.res()` и `.resAll()` больше не пишут ошибки запроса в `console.error`:
179
+ в `AxiosError` лежит конфигурация запроса с токеном авторизации, и он мог
180
+ попасть в журналы браузера и системы сбора ошибок. Ошибка по-прежнему приходит
181
+ подписчику. Токен остаётся в `cause.config` у `BXRestApiError` — не логируйте
182
+ и не отправляйте `cause` целиком.
183
+ - Минимальная версия `axios` поднята до 1.21.0: в версиях до 1.19.0 включительно
184
+ есть уязвимости высокой степени. В `package-lock.json` также обновлён
185
+ `follow-redirects` до 1.16.2.
186
+
10
187
  ## [1.5.0] - 2026-11-02
11
188
 
12
189
  ### Added
package/README.en.md CHANGED
@@ -24,6 +24,9 @@ Also known as: Bitrix24 REST client, BX24 REST SDK, Bitrix24 API client, Bitrix
24
24
  - [A token from the portal without BX24.js](#a-token-from-the-portal-without-bx24js)
25
25
  - [Recipe: straight to the portal](#recipe-straight-to-the-portal)
26
26
  - [Recipe: through a backend proxy](#recipe-through-a-backend-proxy)
27
+ - [Several portals](#several-portals)
28
+ - [Retry, timeout and request queue](#retry-timeout-and-request-queue)
29
+ - [Your own middleware](#your-own-middleware)
27
30
  - [Working in an iframe without BX24.js](#working-in-an-iframe-without-bx24js)
28
31
  - [Usage in Angular](#usage-in-angular)
29
32
  - [SessionKeyServices](#sessionkeyservices)
@@ -93,7 +96,7 @@ Do not hardcode or commit a production access token. Pass it from your applicati
93
96
 
94
97
  ### Result modes
95
98
 
96
- REST calls are lazy: the request starts when the returned `Observable` is subscribed to or passed to `firstValueFrom`.
99
+ REST calls are lazy: the request starts when the returned `Observable` is subscribed to or passed to `firstValueFrom`. Unsubscribing before the response aborts the HTTP request — this is what `switchMap`, `takeUntil` and the RxJS `timeout` operator rely on. Aborting does not undo what the portal has already done: if `crm.deal.add` reached the server, the deal may be created.
97
100
 
98
101
  | Method | Result |
99
102
  | --- | --- |
@@ -196,7 +199,9 @@ For cross-origin requests, the server — the portal or your proxy — must allo
196
199
  | --- | --- |
197
200
  | Bitrix API error with a non-2xx HTTP status — this is how Bitrix sends most errors: `expired_token` (401), `ERROR_METHOD_NOT_FOUND` (404), `QUERY_LIMIT_EXCEEDED` (503), `Not found` from `crm.*.get` (400) | The returned `Observable` errors with a `BXRestApiError`, with or without `throwOnApiError`. |
198
201
  | Bitrix API error in the body of a 2xx response | By default, `.res()` resolves normally with `undefined` — **it does not throw**. Pass `{ throwOnApiError: true }` to make it throw a `BXRestApiError` instead. |
199
- | Transport / configuration errors — network failure, missing auth, missing base URL, a response that is not from Bitrix (e.g. a proxy's HTML error page) | The returned `Observable` errors with a plain `Error`. The original error is in its `cause`: for a response that is not from Bitrix it is the axios error, with the HTTP status in `cause.response.status`. |
202
+ | Transport / configuration errors — network failure, [`timeout`](#retry-timeout-and-request-queue) exceeded, missing auth, an empty base address (see below), a response that is not from Bitrix (e.g. a proxy's HTML error page) | The returned `Observable` errors with a plain `Error`. The original error is in its `cause`: for a response that is not from Bitrix it is the axios error, with the HTTP status in `cause.response.status`; for `timeout` it is `BXRestTimeoutError`. |
203
+
204
+ **Base address.** The request address is built from two parts: the portal address and `urls.additional_part`. The portal address is `urls.key`, or, with `urls.source: 'localStorage'`, the value stored in `localStorage` under the `urls.key` key. The `Base URL was not resolved` error happens only when both parts are empty. If only the portal address is empty and `additional_part` is `'rest'` (as in the default settings), the request goes to `rest/<method>` relative to the current page. This is meant for pages opened on the portal's own domain. But a request also goes there when nothing was found in `localStorage` under the key, so check the value beforehand.
200
205
 
201
206
  `BXRestApiError` fields:
202
207
 
@@ -210,6 +215,8 @@ For cross-origin requests, the server — the portal or your proxy — must allo
210
215
 
211
216
  For REST v3 methods, `error` holds the error `code` and `error_description` its `message`.
212
217
 
218
+ **The token in errors.** An axios error keeps the whole request that was sent: `config.url` holds the address, and with it the webhook secret, and `config.data` holds the body with the token. The library passes this error on as is — in `err.cause` for `.res()` and as the error itself for `.resVanilla()`. So `console.error(err)` or sending the error to an error tracker passes the token along too. If errors go to logs, pick the fields you need, for example `err.status`, `err.error` and `err.cause?.response?.data`.
219
+
213
220
  **Recommended — `.res({ throwOnApiError: true })`.** Every Bitrix error then arrives as a `BXRestApiError` whatever the HTTP status, and can be handled alongside transport errors in a single `catchError`/`error` callback:
214
221
 
215
222
  ```typescript
@@ -238,7 +245,7 @@ bxRest.crm.deal.list({
238
245
 
239
246
  Without `throwOnApiError`, plain `.res()` can't tell "no records" apart from "an error in a 2xx response" — both resolve as `undefined`.
240
247
 
241
- **Alternative — check the raw response.** `.resVanilla()` returns the raw Bitrix body for 2xx responses; check it with the exported `isBXRestAnswerSuccess()` type guard. Note that `.resVanilla()`, `.mapForVanilla()` and `.resAll()` don't convert errors: on a Bitrix error with a non-2xx status they fail with the original axios error, with the response body in `err.response.data`. An error in the body of a 2xx response comes back as is from `.resVanilla()` and `.mapForVanilla()`, while `.resAll()` fails with a `BXRestApiError` — it has no `throwOnApiError` option.
248
+ **Alternative — check the raw response.** `.resVanilla()` returns the raw Bitrix body for 2xx responses; check it with the exported `isBXRestAnswerSuccess()` type guard. Note that `.resVanilla()`, `.mapForVanilla()` and `.resAll()` don't convert errors: on a Bitrix error with a non-2xx status they fail with the original axios error, with the response body in `err.response.data`. An error in the body of a 2xx response comes back as is from `.resVanilla()` and `.mapForVanilla()`, while `.resAll()` fails with a `BXRestApiError` — it has no `throwOnApiError` option. It fails the same way when any page inside a batch request returns an error (`result_error`): `.resAll()` never gives a partial list.
242
249
 
243
250
  ```typescript
244
251
  import { firstValueFrom } from 'rxjs'
@@ -305,7 +312,7 @@ How the retry works:
305
312
  })
306
313
  ```
307
314
 
308
- - **Where it works.** In every library request: `.res()`, `.resAll()` (the first page and every batch request), `.resVanilla()`, `.mapForVanilla()`, `BXRestRequest.post()`/`.get()`, `BXRest.batch()`. The retry lives in the HTTP layer, so `.res()` already gets the result of the retry.
315
+ - **Where it works.** In every library request: `.res()`, `.resAll()` (the first page and every batch request), `.resVanilla()`, `.mapForVanilla()`, `BXRestRequest.post()`/`.get()`. The retry lives in the HTTP layer, so `.res()` already gets the result of the retry.
309
316
  - **What is not retried:** errors in the body of a 2xx response (Bitrix sends `expired_token` with status 401) and errors of single commands inside a successful batch response (`result_error`) — the whole HTTP request is what gets retried.
310
317
 
311
318
  ### A token from the portal without BX24.js
@@ -402,7 +409,105 @@ BXRestSettings.update({
402
409
  })
403
410
  ```
404
411
 
405
- Errors of the refresh function end up in `authRefreshError` of the original error, and `.res()` may print the original error to the console — so keep tokens out of the error messages of the refresh function.
412
+ Errors of the refresh function end up in `authRefreshError` of the original error, and your app may print that error to the console or send it to an error tracker — so keep tokens out of the error messages of the refresh function.
413
+
414
+ ## Several portals
415
+
416
+ `BXRestSettings` are shared by the whole app. To work with several portals in one process, for example on a server that serves different customers, pass the settings to the `BXRestNavvy` constructor. Such a client has its own settings: all its requests go to its portal with its token, and `BXRestSettings` do not affect it.
417
+
418
+ ```typescript
419
+ import { BXRestNavvy } from 'bx-rest'
420
+ import { firstValueFrom } from 'rxjs'
421
+
422
+ const portalA = new BXRestNavvy({
423
+ auth: { source: () => tokenA, key: 'auth' },
424
+ urls: { source: 'string', key: 'https://a.bitrix24.com', additional_part: 'rest' }
425
+ })
426
+ const portalB = new BXRestNavvy({
427
+ auth: { source: () => tokenB, key: 'auth' },
428
+ urls: { source: 'string', key: 'https://b.bitrix24.com', additional_part: 'rest' }
429
+ })
430
+
431
+ const deals = await firstValueFrom(
432
+ portalA.crm.deal.list({ select: ['ID'], filter: {}, start: 0 }).resAll()
433
+ )
434
+ ```
435
+
436
+ - **Without an argument** `new BXRestNavvy()` uses the global `BXRestSettings`, as before.
437
+ - **The format** is the same as for `BXRestSettings.update()`, but the settings are passed in full: `auth` and `urls` are required and are not merged with the global settings.
438
+ - **Changing the token of an existing client.** The library calls `auth.source` before every request, at subscribe time. To change the token, change the value the function returns. Requests created earlier but not started yet get the new token too:
439
+
440
+ ```typescript
441
+ let token = await getToken()
442
+ const portal = new BXRestNavvy({
443
+ auth: {
444
+ source: () => token,
445
+ key: 'auth',
446
+ refresh: async () => { token = await getNewToken() } // optional
447
+ },
448
+ urls: { source: 'string', key: 'https://a.bitrix24.com', additional_part: 'rest' }
449
+ })
450
+
451
+ token = newToken // the next request goes out with the new token
452
+ ```
453
+
454
+ - **Another portal or other headers** — create a new client, it takes a fraction of a millisecond.
455
+ - **Authorization refresh** is per client: a client's `auth.refresh` is called only for its own requests.
456
+ - **`auth.source: 'cookies'` and `'localStorage'`** read the shared browser storage, so two clients with the same key get the same token. For several portals, set `source` to a function.
457
+ - **Objects created directly** — `new Navvy()`, `new BXRestRequest()`, `new SessionKeyServices()` — use the global `BXRestSettings`. Only `BXRestNavvy` and everything created through it get the client's own settings.
458
+
459
+ ## Retry, timeout and request queue
460
+
461
+ Three optional settings protect against the Bitrix24 request rate limit and against hanging requests. Set them in `BXRestSettings.update()` for the whole app or in the settings of a `new BXRestNavvy(settings)` client. `true` turns on the defaults, a number or an object sets your own values. While a setting is not set, requests go as before.
462
+
463
+ ```typescript
464
+ BXRestSettings.update({
465
+ retryOnQueryLimit: true, // 3 attempts, pause from 1000 ms
466
+ timeout: 30_000, // ms
467
+ throttle: { perSecond: 2 },
468
+ })
469
+ ```
470
+
471
+ | Setting | `true` means | What it does |
472
+ | --- | --- | --- |
473
+ | `retryOnQueryLimit` | `{ attempts: 3, delay: 1000 }` | Retries the request when the portal answers `QUERY_LIMIT_EXCEEDED` (HTTP 503, request rate limit exceeded). `attempts` is the total number of attempts including the first one, `delay` is the pause before the second attempt in ms, then it doubles: 1000, 2000, 4000... Other errors are not retried. When the attempts run out, the error of the last one goes out; in `.res()` it is a `BXRestApiError` with `error: 'QUERY_LIMIT_EXCEEDED'`. |
474
+ | `timeout` | `60000` | Time limit for one HTTP request in ms. A request that takes longer is aborted with `BXRestTimeoutError` (its `timeout` field is the limit). In `.res()` it is in `cause`; `.resVanilla()`, `.mapForVanilla()` and `.resAll()` error with it directly. |
475
+ | `throttle` | `{ perSecond: 2 }` | At most `perSecond` requests per second, the rest wait in a queue. 2 requests per second is the Bitrix24 limit on regular plans. |
476
+
477
+ An object may set only some fields: `retryOnQueryLimit: { attempts: 5 }` keeps `delay: 1000`.
478
+
479
+ - **Order:** retry, queue, timeout, [your own middleware](#your-own-middleware), axios. Every retry attempt queues again and gets its own `timeout`. Waiting in the queue and pauses before a retry do not count towards `timeout`.
480
+ - **The queue** belongs to each `new BXRestNavvy(settings)` client and is shared by everything that uses the global `BXRestSettings`. Every request goes through it, including every batch request of `.resAll()`.
481
+ - **`throttle` is stricter than the portal.** Bitrix24 lets short bursts above the limit through, while `throttle` spreads all requests evenly. If short bursts of requests need to be fast, `retryOnQueryLimit` alone is enough.
482
+ - **Unsubscribing** stops both the wait in the queue and the pause before a retry: such a request is never sent.
483
+ - **Settings are read at subscribe time**, like the rest.
484
+ - **An invalid value** — for example `throttle: { perSecond: 0 }` or a negative `timeout` — fails the request with a `TypeError` that describes it; nothing is sent to the portal.
485
+
486
+ ### Your own middleware
487
+
488
+ A middleware is a function every request passes through before it is sent: logging, metrics, changing headers, your own transport instead of axios. Set it in `middleware`, globally or per client in the same way:
489
+
490
+ ```typescript
491
+ import { BXRestMiddleware, BXRestSettings } from 'bx-rest'
492
+
493
+ const logDuration: BXRestMiddleware = async (config, next) => {
494
+ const started = Date.now()
495
+ try {
496
+ return await next(config)
497
+ } finally {
498
+ console.debug(config.method, Date.now() - started, 'ms')
499
+ }
500
+ }
501
+
502
+ BXRestSettings.update({ middleware: [logDuration] })
503
+ ```
504
+
505
+ - **`config`** is the request: `method` (`'GET'` or `'POST'`), `url` (with the parameters for GET), `data` (the POST body), `headers`, `withCredentials`, `signal`. `next(config)` sends it on and returns the response body.
506
+ - **Order:** the first in the list is the outermost one: it sees the request first and the response last. All your middleware run after `retryOnQueryLimit`, `throttle` and `timeout`, so they see every retry attempt. The retry after `auth.refresh` wraps all the steps: on an authorization refresh the request passes through them again.
507
+ - **To change the request**, pass a new object on: `next({ ...config, headers: { ...config.headers, 'X-Trace': id } })`.
508
+ - **Your own transport** is a middleware that does not call `next`: it sends the request itself and returns the response body. For a non-2xx response it must reject with an error that has a `response: { status, data }` field, as axios does. The library uses that field to recognize Bitrix errors (`BXRestApiError`) and authorization errors (`auth.refresh`).
509
+ - **Pass `config.signal`** to your transport: it aborts the request on unsubscribe and on `timeout`.
510
+ - **`config.url` and `config.data`** contain the token and the webhook secret — do not log them as they are.
406
511
 
407
512
  ## Working in an iframe without BX24.js
408
513
 
@@ -665,6 +770,12 @@ npm run demo:bootstrap
665
770
  Then, in any of `projects/demo-angular|demo-vue|demo-react`: `npm run dev`
666
771
  (for Angular, `npm start`). See each demo's own README for details.
667
772
 
773
+ `projects/demo-node` is a Node.js demo without a UI. It shows two portals in
774
+ one process, `retryOnQueryLimit`, `throttle`, `timeout`, your own `middleware`
775
+ for logging and handling `BXRestApiError`. `npm run mock` runs it against a
776
+ built-in mock portal, `npm start` against a real portal using the incoming
777
+ webhook address from `BX_WEBHOOK_A`. On a portal the demo only reads data.
778
+
668
779
  ## Custom REST methods
669
780
 
670
781
  `Navvy` is exported from `bx-rest`. In Angular, register the `NavvyProvider` shown above and inject `Navvy` into your custom API service. A method URL is represented as an array and is joined with dots before the request is sent.
package/README.md CHANGED
@@ -24,6 +24,9 @@
24
24
  - [Токен от портала без BX24.js](#токен-от-портала-без-bx24js)
25
25
  - [Рецепт: напрямую в портал](#рецепт-напрямую-в-портал)
26
26
  - [Рецепт: через бэкенд-прокси](#рецепт-через-бэкенд-прокси)
27
+ - [Несколько порталов](#несколько-порталов)
28
+ - [Повтор, таймаут и очередь запросов](#повтор-таймаут-и-очередь-запросов)
29
+ - [Свои middleware](#свои-middleware)
27
30
  - [Работа в iframe без BX24.js](#работа-в-iframe-без-bx24js)
28
31
  - [Использование в Angular](#использование-в-angular)
29
32
  - [SessionKeyServices](#sessionkeyservices)
@@ -103,7 +106,7 @@ console.log(deals)
103
106
 
104
107
  ### Режимы результата
105
108
 
106
- REST-вызовы ленивые: запрос начинается, когда на возвращённый `Observable` подписываются или передают его в `firstValueFrom`.
109
+ REST-вызовы ленивые: запрос начинается, когда на возвращённый `Observable` подписываются или передают его в `firstValueFrom`. Отписка до ответа отменяет HTTP-запрос — так работают `switchMap`, `takeUntil` и оператор `timeout` из RxJS. Отмена не откатывает то, что портал уже успел сделать: если `crm.deal.add` дошёл до сервера, сделка может быть создана.
107
110
 
108
111
  | Метод | Результат |
109
112
  | --- | --- |
@@ -206,7 +209,9 @@ const deals = await firstValueFrom(
206
209
  | --- | --- |
207
210
  | Ошибка API Bitrix с HTTP-статусом не 2xx — так Bitrix отдаёт большинство ошибок: `expired_token` (401), `ERROR_METHOD_NOT_FOUND` (404), `QUERY_LIMIT_EXCEEDED` (503), `Not found` у `crm.*.get` (400) | Возвращённый `Observable` завершается ошибкой `BXRestApiError` — с `throwOnApiError` и без него. |
208
211
  | Ошибка API Bitrix в теле ответа со статусом 2xx | По умолчанию `.res()` резолвится обычным образом со значением `undefined` — **исключение не выбрасывается**. Передайте `{ throwOnApiError: true }`, чтобы вместо этого выбрасывалось `BXRestApiError`. |
209
- | Ошибки транспорта / конфигурации — сбой сети, отсутствие авторизации, отсутствие базового URL, ответ не от Bitrix (например, HTML-страница ошибки прокси) | Возвращённый `Observable` завершается обычной ошибкой `Error`. Исходная ошибка — в её `cause`: для ответа не от Bitrix это ошибка axios, HTTP-статус — в `cause.response.status`. |
212
+ | Ошибки транспорта / конфигурации — сбой сети, превышен [`timeout`](#повтор-таймаут-и-очередь-запросов), отсутствие авторизации, пустой базовый адрес (см. ниже), ответ не от Bitrix (например, HTML-страница ошибки прокси) | Возвращённый `Observable` завершается обычной ошибкой `Error`. Исходная ошибка — в её `cause`: для ответа не от Bitrix это ошибка axios, HTTP-статус — в `cause.response.status`; для `timeout` — `BXRestTimeoutError`. |
213
+
214
+ **Базовый адрес.** Адрес запроса собирается из двух частей: адреса портала и `urls.additional_part`. Адрес портала — это `urls.key`, а при `urls.source: 'localStorage'` — значение в `localStorage` под ключом `urls.key`. Ошибка `Base URL was not resolved` возникает, только если пусты обе части. Если пуст только адрес портала, а `additional_part` равен `'rest'` (так в настройках по умолчанию), запрос уходит на `rest/<метод>` относительно текущей страницы. Это нужно страницам, открытым на домене самого портала. Но так же уйдёт запрос, если в `localStorage` под ключом ничего не оказалось, — проверяйте значение заранее.
210
215
 
211
216
  Поля `BXRestApiError`:
212
217
 
@@ -220,6 +225,8 @@ const deals = await firstValueFrom(
220
225
 
221
226
  Для методов REST v3 в `error` попадает `code` ошибки, а в `error_description` — её `message`.
222
227
 
228
+ **Токен в ошибках.** Ошибка axios хранит весь отправленный запрос: в `config.url` — адрес, а с ним и секрет вебхука, в `config.data` — тело с токеном. Библиотека отдаёт эту ошибку как есть — в `err.cause` у `.res()` и саму ошибку у `.resVanilla()`. Поэтому `console.error(err)` или отправка ошибки в систему сбора ошибок передаёт и токен. Если ошибки уходят в логи, выбирайте из них нужные поля, например `err.status`, `err.error` и `err.cause?.response?.data`.
229
+
223
230
  **Рекомендуемый вариант — `.res({ throwOnApiError: true })`.** Тогда любая ошибка Bitrix приходит как `BXRestApiError` независимо от HTTP-статуса, и её можно обработать вместе с транспортными ошибками в одном колбэке `catchError`/`error`:
224
231
 
225
232
  ```typescript
@@ -248,7 +255,7 @@ bxRest.crm.deal.list({
248
255
 
249
256
  Без `throwOnApiError` по голому `.res()` нельзя отличить «нет записей» от «ошибки в ответе 2xx» — оба случая резолвятся как `undefined`.
250
257
 
251
- **Альтернатива — проверять сырой ответ.** `.resVanilla()` возвращает сырое тело ответа Bitrix для ответов 2xx; проверяйте его экспортируемым type guard `isBXRestAnswerSuccess()`. Учтите, что `.resVanilla()`, `.mapForVanilla()` и `.resAll()` ошибки не преобразуют: на ошибку Bitrix со статусом не 2xx они завершаются исходной ошибкой axios, тело ответа — в `err.response.data`. Ошибку в теле ответа 2xx `.resVanilla()` и `.mapForVanilla()` отдают как есть, а `.resAll()` завершается ошибкой `BXRestApiError`: опции `throwOnApiError` у него нет.
258
+ **Альтернатива — проверять сырой ответ.** `.resVanilla()` возвращает сырое тело ответа Bitrix для ответов 2xx; проверяйте его экспортируемым type guard `isBXRestAnswerSuccess()`. Учтите, что `.resVanilla()`, `.mapForVanilla()` и `.resAll()` ошибки не преобразуют: на ошибку Bitrix со статусом не 2xx они завершаются исходной ошибкой axios, тело ответа — в `err.response.data`. Ошибку в теле ответа 2xx `.resVanilla()` и `.mapForVanilla()` отдают как есть, а `.resAll()` завершается ошибкой `BXRestApiError`: опции `throwOnApiError` у него нет. Так же он завершается, если ошибку вернула любая из страниц внутри batch-запроса (`result_error`): неполный список `.resAll()` не отдаёт.
252
259
 
253
260
  ```typescript
254
261
  import { firstValueFrom } from 'rxjs'
@@ -315,7 +322,7 @@ BXRestSettings.update({
315
322
  })
316
323
  ```
317
324
 
318
- - **Где работает.** Во всех запросах библиотеки: `.res()`, `.resAll()` (и первая страница, и каждый batch-запрос), `.resVanilla()`, `.mapForVanilla()`, `BXRestRequest.post()`/`.get()`, `BXRest.batch()`. Повтор встроен в HTTP-слой, поэтому `.res()` получает уже результат повтора.
325
+ - **Где работает.** Во всех запросах библиотеки: `.res()`, `.resAll()` (и первая страница, и каждый batch-запрос), `.resVanilla()`, `.mapForVanilla()`, `BXRestRequest.post()`/`.get()`. Повтор встроен в HTTP-слой, поэтому `.res()` получает уже результат повтора.
319
326
  - **Что не повторяется:** ошибки в теле ответа со статусом 2xx (Bitrix отдаёт `expired_token` со статусом 401) и ошибки отдельных команд внутри успешного batch-ответа (`result_error`) — повторяется HTTP-запрос целиком.
320
327
 
321
328
  ### Токен от портала без BX24.js
@@ -412,7 +419,105 @@ BXRestSettings.update({
412
419
  })
413
420
  ```
414
421
 
415
- Ошибки функции продления попадают в `authRefreshError` исходной ошибки, а `.res()` может вывести исходную ошибку в консоль — поэтому не кладите токены в текст ошибок функции продления.
422
+ Ошибки функции продления попадают в `authRefreshError` исходной ошибки, а приложение может вывести эту ошибку в консоль или отправить в систему сбора ошибок — поэтому не кладите токены в текст ошибок функции продления.
423
+
424
+ ## Несколько порталов
425
+
426
+ `BXRestSettings` — общие настройки на всё приложение. Если в одном процессе нужно работать с несколькими порталами, например на сервере, который обслуживает разных клиентов, передайте настройки в конструктор `BXRestNavvy`. У такого клиента настройки свои: все его запросы идут на его портал с его токеном, а `BXRestSettings` на него не влияют.
427
+
428
+ ```typescript
429
+ import { BXRestNavvy } from 'bx-rest'
430
+ import { firstValueFrom } from 'rxjs'
431
+
432
+ const portalA = new BXRestNavvy({
433
+ auth: { source: () => tokenA, key: 'auth' },
434
+ urls: { source: 'string', key: 'https://a.bitrix24.ru', additional_part: 'rest' }
435
+ })
436
+ const portalB = new BXRestNavvy({
437
+ auth: { source: () => tokenB, key: 'auth' },
438
+ urls: { source: 'string', key: 'https://b.bitrix24.ru', additional_part: 'rest' }
439
+ })
440
+
441
+ const deals = await firstValueFrom(
442
+ portalA.crm.deal.list({ select: ['ID'], filter: {}, start: 0 }).resAll()
443
+ )
444
+ ```
445
+
446
+ - **Без аргумента** `new BXRestNavvy()` работает с глобальными `BXRestSettings`, как и раньше.
447
+ - **Формат** тот же, что у `BXRestSettings.update()`, но настройки передаются целиком: `auth` и `urls` обязательны, с глобальными настройками они не смешиваются.
448
+ - **Смена токена у готового клиента.** Функцию `auth.source` библиотека вызывает перед каждым запросом, в момент подписки. Чтобы сменить токен, поменяйте значение, которое она возвращает. Новый токен получат и запросы, созданные раньше, но ещё не запущенные:
449
+
450
+ ```typescript
451
+ let token = await getToken()
452
+ const portal = new BXRestNavvy({
453
+ auth: {
454
+ source: () => token,
455
+ key: 'auth',
456
+ refresh: async () => { token = await getNewToken() } // необязательно
457
+ },
458
+ urls: { source: 'string', key: 'https://a.bitrix24.ru', additional_part: 'rest' }
459
+ })
460
+
461
+ token = newToken // следующий запрос уйдёт с новым токеном
462
+ ```
463
+
464
+ - **Другой портал или другие заголовки** — создайте новый клиент, это занимает доли миллисекунды.
465
+ - **Продление авторизации** у каждого клиента своё: `auth.refresh` клиента вызывается только для его запросов.
466
+ - **`auth.source: 'cookies'` и `'localStorage'`** читают общее хранилище браузера, поэтому два клиента с одним ключом получат один и тот же токен. Для нескольких порталов задавайте `source` функцией.
467
+ - **Объекты, созданные напрямую**, — `new Navvy()`, `new BXRestRequest()`, `new SessionKeyServices()` — работают с глобальными `BXRestSettings`. Свои настройки получает только `BXRestNavvy` и всё, что создаётся через него.
468
+
469
+ ## Повтор, таймаут и очередь запросов
470
+
471
+ Три необязательные настройки защищают от лимита частоты запросов Bitrix24 и от зависших запросов. Задайте их в `BXRestSettings.update()` для всего приложения или в настройках клиента `new BXRestNavvy(settings)`. `true` включает значения по умолчанию, число или объект задают свои. Пока настройка не задана, запросы идут как раньше.
472
+
473
+ ```typescript
474
+ BXRestSettings.update({
475
+ retryOnQueryLimit: true, // 3 попытки, пауза от 1000 мс
476
+ timeout: 30_000, // мс
477
+ throttle: { perSecond: 2 },
478
+ })
479
+ ```
480
+
481
+ | Настройка | `true` означает | Что делает |
482
+ | --- | --- | --- |
483
+ | `retryOnQueryLimit` | `{ attempts: 3, delay: 1000 }` | Повторяет запрос, если портал ответил `QUERY_LIMIT_EXCEEDED` (HTTP 503, превышен лимит частоты запросов). `attempts` — сколько всего попыток вместе с первой, `delay` — пауза перед второй попыткой в мс, дальше она растёт вдвое: 1000, 2000, 4000... Другие ошибки не повторяются. Попытки кончились — наружу уходит ошибка последней, в `.res()` это `BXRestApiError` с `error: 'QUERY_LIMIT_EXCEEDED'`. |
484
+ | `timeout` | `60000` | Предельное время одного HTTP-запроса в мс. Не уложился — запрос отменяется с ошибкой `BXRestTimeoutError` (поле `timeout` — заданное время). В `.res()` она лежит в `cause`, `.resVanilla()`, `.mapForVanilla()` и `.resAll()` завершаются ей самой. |
485
+ | `throttle` | `{ perSecond: 2 }` | Не больше `perSecond` запросов в секунду, остальные ждут своей очереди. 2 запроса в секунду — лимит Bitrix24 на обычных тарифах. |
486
+
487
+ В объекте можно задать только часть полей: `retryOnQueryLimit: { attempts: 5 }` оставит `delay: 1000`.
488
+
489
+ - **Порядок:** повтор, очередь, таймаут, [свои middleware](#свои-middleware), axios. Каждая попытка повтора заново встаёт в очередь и получает своё время `timeout`. Ожидание в очереди и паузы перед повтором в `timeout` не входят.
490
+ - **Очередь** своя у каждого клиента `new BXRestNavvy(settings)` и общая у всех, кто работает с глобальными `BXRestSettings`. Через неё проходит каждый запрос, в том числе каждый batch-запрос `.resAll()`.
491
+ - **`throttle` строже портала.** Bitrix24 пропускает короткие всплески сверх лимита, а `throttle` равномерно распределяет все запросы. Если важна скорость коротких пачек запросов, хватит одного `retryOnQueryLimit`.
492
+ - **Отписка** прерывает и ожидание в очереди, и паузу перед повтором: такой запрос уже не уйдёт.
493
+ - **Настройки читаются в момент подписки**, как и остальные.
494
+ - **Неверное значение** — например, `throttle: { perSecond: 0 }` или отрицательный `timeout` — завершает запрос ошибкой `TypeError` с описанием, на портал ничего не уходит.
495
+
496
+ ### Свои middleware
497
+
498
+ Middleware — функция, через которую проходит каждый запрос перед отправкой: логирование, метрики, подмена заголовков, свой транспорт вместо axios. Задаётся в `middleware` — так же глобально или у клиента:
499
+
500
+ ```typescript
501
+ import { BXRestMiddleware, BXRestSettings } from 'bx-rest'
502
+
503
+ const logDuration: BXRestMiddleware = async (config, next) => {
504
+ const started = Date.now()
505
+ try {
506
+ return await next(config)
507
+ } finally {
508
+ console.debug(config.method, Date.now() - started, 'ms')
509
+ }
510
+ }
511
+
512
+ BXRestSettings.update({ middleware: [logDuration] })
513
+ ```
514
+
515
+ - **`config`** — запрос: `method` (`'GET'` или `'POST'`), `url` (у GET вместе с параметрами), `data` (тело POST), `headers`, `withCredentials`, `signal`. `next(config)` отправляет его дальше и возвращает тело ответа.
516
+ - **Порядок:** первый в списке — внешний, он первым видит запрос и последним — ответ. Все свои middleware идут после `retryOnQueryLimit`, `throttle` и `timeout`, поэтому видят каждую попытку повтора. Повтор после `auth.refresh` идёт снаружи всех шагов: при продлении авторизации запрос пройдёт через них ещё раз.
517
+ - **Изменить запрос** — передать дальше новый объект: `next({ ...config, headers: { ...config.headers, 'X-Trace': id } })`.
518
+ - **Свой транспорт** — middleware, который не зовёт `next`: он сам отправляет запрос и возвращает тело ответа. На ответ не 2xx он должен отклонить промис ошибкой с полем `response: { status, data }`, как это делает axios. По этому полю библиотека узнаёт ошибки Bitrix (`BXRestApiError`) и ошибки авторизации (`auth.refresh`).
519
+ - **`config.signal`** передавайте в свой транспорт: по нему запрос отменяется при отписке и по `timeout`.
520
+ - **`config.url` и `config.data`** содержат токен и секрет вебхука — не логируйте их как есть.
416
521
 
417
522
  ## Работа в iframe без BX24.js
418
523
 
@@ -675,6 +780,13 @@ npm run demo:bootstrap
675
780
  Дальше в любой из `projects/demo-angular|demo-vue|demo-react`: `npm run dev`
676
781
  (для Angular — `npm start`). Подробности — в README каждого демо.
677
782
 
783
+ `projects/demo-node` — демо для Node.js, без интерфейса. Показывает два
784
+ портала в одном процессе, `retryOnQueryLimit`, `throttle`, `timeout`, свой
785
+ `middleware` для логирования и обработку `BXRestApiError`. `npm run mock`
786
+ запускает его со встроенной заглушкой портала, `npm start` — с настоящим
787
+ порталом по адресу входящего вебхука из `BX_WEBHOOK_A`. На портале демо только
788
+ читает данные.
789
+
678
790
  ## Кастомные REST-методы
679
791
 
680
792
  `Navvy` экспортируется из `bx-rest`. В Angular зарегистрируйте показанный выше `NavvyProvider` и внедрите `Navvy` в ваш кастомный API-сервис. URL метода представлен массивом и перед отправкой запроса соединяется точками.