itd-api 0.0.11 → 0.1.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 (41) hide show
  1. package/README.md +8 -4
  2. package/dist/{chunk-TB7HW3VX.js → chunk-6FB4HTKH.js} +660 -174
  3. package/dist/chunk-6FB4HTKH.js.map +1 -0
  4. package/dist/{chunk-QD4UHJFF.cjs → chunk-73CISRBG.cjs} +660 -174
  5. package/dist/chunk-73CISRBG.cjs.map +1 -0
  6. package/dist/{index-CrlTO7sR.d.cts → index-BZF4K90s.d.cts} +125 -22
  7. package/dist/{index-CrlTO7sR.d.ts → index-BZF4K90s.d.ts} +125 -22
  8. package/dist/index.cjs +109 -109
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/node.cjs +112 -112
  13. package/dist/node.d.cts +2 -2
  14. package/dist/node.d.ts +2 -2
  15. package/dist/node.js +2 -2
  16. package/guides/README.md +2 -0
  17. package/guides/multi-accounts/README.md +2 -1
  18. package/guides/plugins/README.md +93 -2
  19. package/guides/reference/README.md +67 -0
  20. package/guides/reference/accounts.md +101 -0
  21. package/guides/reference/auth.md +141 -0
  22. package/guides/reference/builders.md +135 -0
  23. package/guides/reference/client.md +184 -0
  24. package/guides/reference/comments.md +58 -0
  25. package/guides/reference/discovery.md +81 -0
  26. package/guides/reference/enums.md +103 -0
  27. package/guides/reference/errors.md +107 -0
  28. package/guides/reference/files.md +73 -0
  29. package/guides/reference/models.md +448 -0
  30. package/guides/reference/notifications.md +77 -0
  31. package/guides/reference/pagination.md +82 -0
  32. package/guides/reference/platform.md +47 -0
  33. package/guides/reference/posts.md +157 -0
  34. package/guides/reference/realtime.md +78 -0
  35. package/guides/reference/reports.md +28 -0
  36. package/guides/reference/subscription.md +41 -0
  37. package/guides/reference/users.md +146 -0
  38. package/guides/reference/verification.md +24 -0
  39. package/package.json +1 -1
  40. package/dist/chunk-QD4UHJFF.cjs.map +0 -1
  41. package/dist/chunk-TB7HW3VX.js.map +0 -1
@@ -621,8 +621,8 @@ var ItdTimeoutError = class extends ItdError {
621
621
  }
622
622
  };
623
623
  var ItdAbortError = class extends ItdError {
624
- constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D") {
625
- super(ItdErrorKind.Abort, message);
624
+ constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D", options) {
625
+ super(ItdErrorKind.Abort, message, options);
626
626
  this.name = "ItdAbortError";
627
627
  }
628
628
  };
@@ -911,6 +911,15 @@ var AuthManager = class {
911
911
  #deviceId;
912
912
  /** Общий промис первичной выдачи `deviceId` — чтобы параллельные запросы получили один. */
913
913
  #deviceIdLoading = null;
914
+ /**
915
+ * Счётчик смен владельца авторизации.
916
+ *
917
+ * Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
918
+ * `setSession`, `setAccessToken`, вход. `#performRefresh` снимает его значение перед
919
+ * сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
920
+ * воскресить уже очищенную сессию поверх `signOut`.
921
+ */
922
+ #authEpoch = 0;
914
923
  constructor(config, send, jar, hooks = {}) {
915
924
  this.#config = config;
916
925
  this.#send = send;
@@ -956,6 +965,10 @@ var AuthManager = class {
956
965
  #rotateAuthScope() {
957
966
  this.#authScope = nextAuthScope();
958
967
  }
968
+ /** Отмечает смену владельца авторизации — обесценивает результат идущего обновления. */
969
+ #invalidateInFlight() {
970
+ this.#authEpoch += 1;
971
+ }
959
972
  #identityForToken(accessToken) {
960
973
  const token = accessToken ? readTokenIdentity(accessToken) : {};
961
974
  return {
@@ -1101,6 +1114,7 @@ var AuthManager = class {
1101
1114
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
1102
1115
  async setAccessToken(accessToken) {
1103
1116
  await this.#loadSession();
1117
+ this.#invalidateInFlight();
1104
1118
  this.#transitionAuth(accessToken);
1105
1119
  await this.#saveSession({ ...this.#session ?? {}, accessToken, obtainedAt: Date.now() });
1106
1120
  this.#emitter.emit("tokens", { accessToken });
@@ -1123,6 +1137,7 @@ var AuthManager = class {
1123
1137
  /** Заменяет сессию и связанные с ней cookie целиком. */
1124
1138
  async setSession(session) {
1125
1139
  await this.#loadSession();
1140
+ this.#invalidateInFlight();
1126
1141
  this.#transitionAuth(session.accessToken);
1127
1142
  this.#jar.clear();
1128
1143
  this.#jar.deserialize(session.cookies);
@@ -1139,6 +1154,7 @@ var AuthManager = class {
1139
1154
  */
1140
1155
  async clear() {
1141
1156
  await this.#loadSession();
1157
+ this.#invalidateInFlight();
1142
1158
  this.#transitionAuth(void 0);
1143
1159
  this.#session = null;
1144
1160
  this.#jar.clear();
@@ -1221,6 +1237,7 @@ var AuthManager = class {
1221
1237
  if (!this.#hasRefreshSession()) {
1222
1238
  return this.#reloginOrNull();
1223
1239
  }
1240
+ const epoch = this.#authEpoch;
1224
1241
  try {
1225
1242
  const payload = await this.#send({
1226
1243
  method: "POST",
@@ -1233,6 +1250,9 @@ var AuthManager = class {
1233
1250
  });
1234
1251
  const accessToken = readAccessToken(payload);
1235
1252
  if (!accessToken) return this.#reloginOrNull();
1253
+ if (this.#authEpoch !== epoch) {
1254
+ return this.#session?.accessToken ?? null;
1255
+ }
1236
1256
  const rotated = this.#jar.getValue(
1237
1257
  REFRESH_COOKIE,
1238
1258
  this.#config.baseUrl + REFRESH_COOKIE_PATH
@@ -1248,6 +1268,9 @@ var AuthManager = class {
1248
1268
  return accessToken;
1249
1269
  } catch (error) {
1250
1270
  if (error instanceof ItdApiError) {
1271
+ if (this.#authEpoch !== epoch) {
1272
+ return this.#session?.accessToken ?? null;
1273
+ }
1251
1274
  this.#transitionAuth(void 0);
1252
1275
  this.#session = null;
1253
1276
  this.#jar.clear();
@@ -1317,6 +1340,7 @@ var AuthManager = class {
1317
1340
  "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u043A\u043E\u0434\u043E\u043C \u0438\u0437 \u043F\u0438\u0441\u044C\u043C\u0430. \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0432\u0445\u043E\u0434 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D: \u0432\u043E\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435\u0441\u044C itd.auth.signInWithOtp() \u0438 \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043D\u044B\u0439 accessToken \u0432 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044E \u043A\u043B\u0438\u0435\u043D\u0442\u0430."
1318
1341
  );
1319
1342
  }
1343
+ this.#invalidateInFlight();
1320
1344
  this.#transitionAuth(accessToken);
1321
1345
  await this.#saveSession({ accessToken, obtainedAt: Date.now() });
1322
1346
  this.#emitter.emit("tokens", { accessToken });
@@ -1384,7 +1408,7 @@ function normalizeBaseUrl(baseUrl) {
1384
1408
  }
1385
1409
 
1386
1410
  // src/core/version.ts
1387
- var LIBRARY_VERSION = "0.0.11";
1411
+ var LIBRARY_VERSION = "0.1.0";
1388
1412
 
1389
1413
  // src/core/config.ts
1390
1414
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
@@ -1669,6 +1693,396 @@ function withLayerHeaders(request, headers) {
1669
1693
  return { ...request, layerHeaders: { ...request.layerHeaders, ...headers } };
1670
1694
  }
1671
1695
 
1696
+ // src/core/plugins.ts
1697
+ var NO_KEYS = /* @__PURE__ */ new Set();
1698
+ var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
1699
+ "signal",
1700
+ "timeout",
1701
+ "headers",
1702
+ "retry",
1703
+ "method",
1704
+ "path",
1705
+ "service",
1706
+ "baseUrl",
1707
+ "query",
1708
+ "body",
1709
+ "skipAuth",
1710
+ "skipAuthRefresh",
1711
+ "skipQueue",
1712
+ "raw"
1713
+ ]);
1714
+ var RELATION_FIELDS = ["requires", "conflicts", "before", "after"];
1715
+ var HOOK_FIELDS = ["onRequest", "onResponse", "onError", "onRetry"];
1716
+ function validateNameList(plugin, field) {
1717
+ const values = plugin[field];
1718
+ if (values === void 0) return;
1719
+ if (!Array.isArray(values)) {
1720
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB: ${field} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043C\u0430\u0441\u0441\u0438\u0432\u043E\u043C`);
1721
+ }
1722
+ const seen = /* @__PURE__ */ new Set();
1723
+ for (const value of values) {
1724
+ if (typeof value !== "string" || value.trim() === "") {
1725
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB: ${field} \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043F\u043B\u0430\u0433\u0438\u043D\u0430`);
1726
+ }
1727
+ if (value === plugin.name) {
1728
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u0441\u0435\u0431\u044F \u0432 ${field}`);
1729
+ }
1730
+ if (seen.has(value)) {
1731
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB: ${field} \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442 \u0438\u043C\u044F \xAB${value}\xBB`);
1732
+ }
1733
+ seen.add(value);
1734
+ }
1735
+ }
1736
+ function validateHooks(plugin, hooks) {
1737
+ if (typeof hooks !== "object" || hooks === null) {
1738
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 useHooks() \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442`);
1739
+ }
1740
+ for (const field of HOOK_FIELDS) {
1741
+ if (hooks[field] !== void 0 && typeof hooks[field] !== "function") {
1742
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin}\xBB: useHooks().${field} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439`);
1743
+ }
1744
+ }
1745
+ }
1746
+ function validatePluginDefinition(plugin) {
1747
+ if (typeof plugin?.install !== "function") {
1748
+ throw new ItdConfigError("\u041F\u043B\u0430\u0433\u0438\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0441 \u043C\u0435\u0442\u043E\u0434\u043E\u043C install()");
1749
+ }
1750
+ const name = plugin.name;
1751
+ if (typeof name !== "string" || name.trim() === "") {
1752
+ throw new ItdConfigError("\u0423 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
1753
+ }
1754
+ const keys = plugin.optionKeys ?? [];
1755
+ if (!Array.isArray(keys)) {
1756
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB: optionKeys \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043C\u0430\u0441\u0441\u0438\u0432\u043E\u043C`);
1757
+ }
1758
+ for (const key of keys) {
1759
+ if (typeof key !== "string" || key.trim() === "") {
1760
+ throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043E\u043F\u0446\u0438\u0438`);
1761
+ }
1762
+ if (RESERVED_OPTION_KEYS.has(key)) {
1763
+ throw new ItdConfigError(
1764
+ `\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043E\u043F\u0446\u0438\u044E \xAB${key}\xBB: \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E. \u0417\u0430\u043D\u044F\u0442\u044B\u0435 \u0438\u043C\u0435\u043D\u0430: ${[...RESERVED_OPTION_KEYS].join(", ")}`
1765
+ );
1766
+ }
1767
+ }
1768
+ for (const field of RELATION_FIELDS) validateNameList(plugin, field);
1769
+ }
1770
+ function addEdge(from, to, edges, indegree) {
1771
+ const targets = edges.get(from);
1772
+ if (!targets || targets.has(to)) return;
1773
+ targets.add(to);
1774
+ indegree.set(to, (indegree.get(to) ?? 0) + 1);
1775
+ }
1776
+ function orderPluginDefinitions(plugins) {
1777
+ const entries = [];
1778
+ const byName = /* @__PURE__ */ new Map();
1779
+ for (const [sequence, plugin] of plugins.entries()) {
1780
+ validatePluginDefinition(plugin);
1781
+ if (byName.has(plugin.name)) {
1782
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
1783
+ }
1784
+ const entry = { plugin, sequence };
1785
+ entries.push(entry);
1786
+ byName.set(plugin.name, entry);
1787
+ }
1788
+ for (const { plugin } of entries) {
1789
+ for (const required of plugin.requires ?? []) {
1790
+ if (!byName.has(required)) {
1791
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D\u0443 \xAB${plugin.name}\xBB \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u043F\u043B\u0430\u0433\u0438\u043D \xAB${required}\xBB`);
1792
+ }
1793
+ }
1794
+ for (const conflict of plugin.conflicts ?? []) {
1795
+ if (byName.has(conflict)) {
1796
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u043D\u0435\u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C \u0441 \u043F\u043B\u0430\u0433\u0438\u043D\u043E\u043C \xAB${conflict}\xBB`);
1797
+ }
1798
+ }
1799
+ for (const { plugin: other } of entries) {
1800
+ if (other.conflicts?.includes(plugin.name)) {
1801
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u043D\u0435\u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C \u0441 \u043F\u043B\u0430\u0433\u0438\u043D\u043E\u043C \xAB${other.name}\xBB`);
1802
+ }
1803
+ }
1804
+ }
1805
+ const edges = /* @__PURE__ */ new Map();
1806
+ const indegree = /* @__PURE__ */ new Map();
1807
+ for (const { plugin } of entries) {
1808
+ edges.set(plugin.name, /* @__PURE__ */ new Set());
1809
+ indegree.set(plugin.name, 0);
1810
+ }
1811
+ for (const { plugin } of entries) {
1812
+ for (const required of plugin.requires ?? []) {
1813
+ addEdge(required, plugin.name, edges, indegree);
1814
+ }
1815
+ for (const target of plugin.before ?? []) {
1816
+ if (byName.has(target)) addEdge(plugin.name, target, edges, indegree);
1817
+ }
1818
+ for (const target of plugin.after ?? []) {
1819
+ if (byName.has(target)) addEdge(target, plugin.name, edges, indegree);
1820
+ }
1821
+ }
1822
+ const ready = entries.filter(({ plugin }) => indegree.get(plugin.name) === 0);
1823
+ ready.sort((a, b) => a.sequence - b.sequence);
1824
+ const ordered = [];
1825
+ while (ready.length > 0) {
1826
+ const current = ready.shift();
1827
+ if (!current) break;
1828
+ ordered.push(current.plugin);
1829
+ for (const target of edges.get(current.plugin.name) ?? []) {
1830
+ const next = (indegree.get(target) ?? 0) - 1;
1831
+ indegree.set(target, next);
1832
+ if (next === 0) {
1833
+ const entry = byName.get(target);
1834
+ if (entry) {
1835
+ ready.push(entry);
1836
+ ready.sort((a, b) => a.sequence - b.sequence);
1837
+ }
1838
+ }
1839
+ }
1840
+ }
1841
+ if (ordered.length !== entries.length) {
1842
+ const cycle = entries.filter(({ plugin }) => (indegree.get(plugin.name) ?? 0) > 0).map(({ plugin }) => plugin.name);
1843
+ throw new ItdConfigError(`\u0446\u0438\u043A\u043B\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u043F\u043B\u0430\u0433\u0438\u043D\u043E\u0432: ${cycle.join(" \u2192 ")}`);
1844
+ }
1845
+ return ordered;
1846
+ }
1847
+ function assertPluginRemovable(plugins, name) {
1848
+ const dependent = plugins.find((plugin) => plugin.requires?.includes(name));
1849
+ if (dependent) {
1850
+ throw new ItdConfigError(
1851
+ `\u043D\u0435\u043B\u044C\u0437\u044F \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB: \u043E\u0442 \u043D\u0435\u0433\u043E \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \xAB${dependent.name}\xBB`
1852
+ );
1853
+ }
1854
+ }
1855
+ var REQUEST_HOOK_DISPATCHERS = /* @__PURE__ */ new WeakMap();
1856
+ var PLUGIN_HOOK_SCOPE = /* @__PURE__ */ Symbol("itd-api.plugin-hooks");
1857
+ async function dispatchRequestHook(hooks, field, context, request) {
1858
+ const dispatcher = REQUEST_HOOK_DISPATCHERS.get(hooks);
1859
+ if (dispatcher) {
1860
+ await dispatcher(field, context, request);
1861
+ return;
1862
+ }
1863
+ const hook = hooks[field];
1864
+ await hook?.(context);
1865
+ }
1866
+ var PluginRegistry = class {
1867
+ #entries = /* @__PURE__ */ new Map();
1868
+ #optionKeys = /* @__PURE__ */ new Set();
1869
+ #removing = /* @__PURE__ */ new Set();
1870
+ #cleanups = /* @__PURE__ */ new Set();
1871
+ #ordered = [];
1872
+ /** Сколько плагинов подключено. */
1873
+ get size() {
1874
+ return this.#entries.size;
1875
+ }
1876
+ /** Имена опций активных плагинов. */
1877
+ get optionKeys() {
1878
+ return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
1879
+ }
1880
+ /** Имена плагинов в фактическом порядке выполнения. */
1881
+ names() {
1882
+ return this.#ordered.map(({ plugin }) => plugin.name);
1883
+ }
1884
+ /** Подключён ли плагин с таким именем. */
1885
+ has(name) {
1886
+ return this.#entries.has(name);
1887
+ }
1888
+ /** Проверяет добавление без вызова `install()`. @internal */
1889
+ assertCanAdd(plugin) {
1890
+ validatePluginDefinition(plugin);
1891
+ if (this.#removing.has(plugin.name)) {
1892
+ throw new ItdConfigError(
1893
+ `\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u0435\u0449\u0451 \u043E\u0442\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F; \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F unuse() \u0438\u043B\u0438 dispose()`
1894
+ );
1895
+ }
1896
+ orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
1897
+ }
1898
+ /** Проверяет удаление без изменения реестра. @internal */
1899
+ assertCanRemove(name) {
1900
+ const entry = this.#entries.get(name);
1901
+ if (!entry) return;
1902
+ assertPluginRemovable(
1903
+ this.#ordered.map((current) => current.plugin),
1904
+ name
1905
+ );
1906
+ }
1907
+ /**
1908
+ * Подключает плагин.
1909
+ *
1910
+ * @throws {ItdConfigError} если плагин задан неверно, уже подключён, нарушает зависимости
1911
+ * или заявил занятое имя опции
1912
+ */
1913
+ add(plugin, context) {
1914
+ this.assertCanAdd(plugin);
1915
+ const ordered = orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
1916
+ const transformers = [];
1917
+ const hooks = [];
1918
+ const installed = plugin.install({
1919
+ ...context,
1920
+ use: (transformer) => {
1921
+ if (typeof transformer !== "function") {
1922
+ throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
1923
+ }
1924
+ transformers.push(transformer);
1925
+ },
1926
+ useHooks: (value) => {
1927
+ validateHooks(plugin.name, value);
1928
+ hooks.push({ ...value });
1929
+ }
1930
+ });
1931
+ const teardown = typeof installed === "function" ? installed : void 0;
1932
+ this.#entries.set(plugin.name, {
1933
+ plugin,
1934
+ transformers,
1935
+ hooks,
1936
+ teardown,
1937
+ activeRequests: 0,
1938
+ drain: void 0,
1939
+ finishDrain: void 0
1940
+ });
1941
+ this.#ordered = ordered.map((definition) => {
1942
+ const entry = this.#entries.get(definition.name);
1943
+ if (!entry) throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${definition.name}\xBB \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D`);
1944
+ return entry;
1945
+ });
1946
+ this.#rebuildOptionKeys();
1947
+ }
1948
+ /**
1949
+ * Отключает плагин и вызывает его функцию очистки.
1950
+ *
1951
+ * Новые запросы перестают видеть плагин сразу. Если его обёртка уже выполняется,
1952
+ * очистка дождётся завершения этого логического запроса.
1953
+ *
1954
+ * @returns `false`, если такого плагина не было
1955
+ */
1956
+ async remove(name) {
1957
+ const entry = this.#entries.get(name);
1958
+ if (!entry) return false;
1959
+ this.assertCanRemove(name);
1960
+ this.#entries.delete(name);
1961
+ this.#ordered = this.#ordered.filter((current) => current !== entry);
1962
+ this.#rebuildOptionKeys();
1963
+ this.#removing.add(name);
1964
+ const cleanup = this.#trackCleanup(
1965
+ (async () => {
1966
+ await this.#waitForDrain(entry);
1967
+ await entry.teardown?.();
1968
+ })()
1969
+ );
1970
+ try {
1971
+ await cleanup;
1972
+ return true;
1973
+ } finally {
1974
+ this.#removing.delete(name);
1975
+ }
1976
+ }
1977
+ /**
1978
+ * Отключает все плагины окончательно.
1979
+ *
1980
+ * Очистка идёт изнутри наружу — в порядке, обратном выполнению обёрток.
1981
+ */
1982
+ async dispose() {
1983
+ const entries = [...this.#ordered].reverse();
1984
+ const previousCleanups = [...this.#cleanups];
1985
+ this.#entries.clear();
1986
+ this.#ordered = [];
1987
+ this.#optionKeys.clear();
1988
+ for (const { plugin } of entries) this.#removing.add(plugin.name);
1989
+ const cleanup = this.#trackCleanup(
1990
+ (async () => {
1991
+ const errors = [];
1992
+ const previous = await Promise.allSettled(previousCleanups);
1993
+ for (const result of previous) {
1994
+ if (result.status === "rejected") errors.push(result.reason);
1995
+ }
1996
+ for (const entry of entries) {
1997
+ try {
1998
+ await this.#waitForDrain(entry);
1999
+ await entry.teardown?.();
2000
+ } catch (error) {
2001
+ errors.push(error);
2002
+ } finally {
2003
+ this.#removing.delete(entry.plugin.name);
2004
+ }
2005
+ }
2006
+ if (errors.length > 0) {
2007
+ throw new AggregateError(errors, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u044C \u0440\u0435\u0441\u0443\u0440\u0441\u044B \u043F\u043B\u0430\u0433\u0438\u043D\u043E\u0432");
2008
+ }
2009
+ })()
2010
+ );
2011
+ await cleanup;
2012
+ }
2013
+ /**
2014
+ * Объединяет конструкторские хуки с хуками подключаемых плагинов.
2015
+ *
2016
+ * Возвращённый объект динамический: подключение и отключение плагина начинает действовать
2017
+ * со следующего логического запроса без пересоздания транспорта.
2018
+ */
2019
+ hooks(base) {
2020
+ const hooks = {};
2021
+ REQUEST_HOOK_DISPATCHERS.set(hooks, ((field, context, request) => this.#runHook(field, context, request, base)));
2022
+ return hooks;
2023
+ }
2024
+ /**
2025
+ * Прогоняет запрос через цепочку обёрток.
2026
+ *
2027
+ * Снимок цепочки берётся в начале: `unuse()` влияет на новые запросы, но не обрывает
2028
+ * уже выполняющийся посередине.
2029
+ *
2030
+ * @param execute настоящий запрос, вызывается самой внутренней обёрткой
2031
+ */
2032
+ async run(request, execute) {
2033
+ const entries = [...this.#ordered];
2034
+ for (const entry of entries) entry.activeRequests += 1;
2035
+ const hookScope = entries.flatMap((entry) => entry.hooks);
2036
+ const scoped = (current) => current[PLUGIN_HOOK_SCOPE] === hookScope ? current : { ...current, [PLUGIN_HOOK_SCOPE]: hookScope };
2037
+ const chain = entries.flatMap((entry) => entry.transformers).reduceRight(
2038
+ (next, transformer) => (current) => transformer(scoped(current), (prepared) => next(scoped(prepared))),
2039
+ (current) => execute(scoped(current))
2040
+ );
2041
+ try {
2042
+ return await chain(scoped(request));
2043
+ } finally {
2044
+ for (const entry of entries) {
2045
+ entry.activeRequests -= 1;
2046
+ if (entry.activeRequests === 0) {
2047
+ entry.finishDrain?.();
2048
+ entry.finishDrain = void 0;
2049
+ entry.drain = void 0;
2050
+ }
2051
+ }
2052
+ }
2053
+ }
2054
+ #rebuildOptionKeys() {
2055
+ this.#optionKeys.clear();
2056
+ for (const { plugin } of this.#ordered) {
2057
+ for (const key of plugin.optionKeys ?? []) this.#optionKeys.add(key);
2058
+ }
2059
+ }
2060
+ #waitForDrain(entry) {
2061
+ if (entry.activeRequests === 0) return Promise.resolve();
2062
+ entry.drain ??= new Promise((resolve) => {
2063
+ entry.finishDrain = resolve;
2064
+ });
2065
+ return entry.drain;
2066
+ }
2067
+ #trackCleanup(cleanup) {
2068
+ this.#cleanups.add(cleanup);
2069
+ void cleanup.then(
2070
+ () => this.#cleanups.delete(cleanup),
2071
+ () => this.#cleanups.delete(cleanup)
2072
+ );
2073
+ return cleanup;
2074
+ }
2075
+ async #runHook(field, context, request, base) {
2076
+ const baseHook = base[field];
2077
+ await baseHook?.(context);
2078
+ const scope = request[PLUGIN_HOOK_SCOPE] ?? [];
2079
+ for (const hooks of scope) {
2080
+ const hook = hooks[field];
2081
+ await hook?.(context);
2082
+ }
2083
+ }
2084
+ };
2085
+
1672
2086
  // src/core/retry.ts
1673
2087
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
1674
2088
  function isRetryable(error, method, retryWrites) {
@@ -1724,10 +2138,7 @@ function createQueueMiddleware(schedule) {
1724
2138
  return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
1725
2139
  }
1726
2140
  function createPluginsMiddleware(plugins) {
1727
- return (request, next) => {
1728
- if (plugins.size === 0) return next(request);
1729
- return plugins.run(request, next);
1730
- };
2141
+ return (request, next) => plugins.run(request, next);
1731
2142
  }
1732
2143
  function createServicesMiddleware(registry) {
1733
2144
  return async (request, next) => {
@@ -1799,16 +2210,21 @@ function createRetryMiddleware(deps) {
1799
2210
  } catch (error) {
1800
2211
  const delay = nextDelay(error, attempt, request, method, backoff);
1801
2212
  if (delay === void 0) throw error;
1802
- await deps.hooks.onRetry?.({
1803
- method,
1804
- path: request.path,
1805
- url: deps.buildUrl(request),
1806
- // Умолчания транспорта добавляются после слоя повторов и сюда не входят.
1807
- headers: new Headers({ ...request.layerHeaders, ...request.headers }),
1808
- attempt,
1809
- error,
1810
- delay
1811
- });
2213
+ await dispatchRequestHook(
2214
+ deps.hooks,
2215
+ "onRetry",
2216
+ {
2217
+ method,
2218
+ path: request.path,
2219
+ url: deps.buildUrl(request),
2220
+ // Умолчания транспорта добавляются после слоя повторов и сюда не входят.
2221
+ headers: new Headers({ ...request.layerHeaders, ...request.headers }),
2222
+ attempt,
2223
+ error,
2224
+ delay
2225
+ },
2226
+ request
2227
+ );
1812
2228
  deps.logger?.debug(
1813
2229
  `\u043F\u043E\u0432\u0442\u043E\u0440 ${method} ${request.path}, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${delay} \u043C\u0441`
1814
2230
  );
@@ -1818,104 +2234,6 @@ function createRetryMiddleware(deps) {
1818
2234
  };
1819
2235
  }
1820
2236
 
1821
- // src/core/plugins.ts
1822
- var NO_KEYS = /* @__PURE__ */ new Set();
1823
- var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
1824
- "signal",
1825
- "timeout",
1826
- "headers",
1827
- "retry",
1828
- "method",
1829
- "path",
1830
- "service",
1831
- "baseUrl",
1832
- "query",
1833
- "body",
1834
- "skipAuth",
1835
- "skipAuthRefresh",
1836
- "skipQueue",
1837
- "raw"
1838
- ]);
1839
- function validatePluginDefinition(plugin) {
1840
- if (typeof plugin?.install !== "function") {
1841
- throw new ItdConfigError("\u041F\u043B\u0430\u0433\u0438\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0441 \u043C\u0435\u0442\u043E\u0434\u043E\u043C install()");
1842
- }
1843
- const name = plugin.name;
1844
- if (typeof name !== "string" || name.trim() === "") {
1845
- throw new ItdConfigError("\u0423 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
1846
- }
1847
- const keys = plugin.optionKeys ?? [];
1848
- for (const key of keys) {
1849
- if (typeof key !== "string" || key.trim() === "") {
1850
- throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043E\u043F\u0446\u0438\u0438`);
1851
- }
1852
- if (RESERVED_OPTION_KEYS.has(key)) {
1853
- throw new ItdConfigError(
1854
- `\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043E\u043F\u0446\u0438\u044E \xAB${key}\xBB: \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E. \u0417\u0430\u043D\u044F\u0442\u044B\u0435 \u0438\u043C\u0435\u043D\u0430: ${[...RESERVED_OPTION_KEYS].join(", ")}`
1855
- );
1856
- }
1857
- }
1858
- }
1859
- var PluginRegistry = class {
1860
- #transformers = [];
1861
- #optionKeys = /* @__PURE__ */ new Set();
1862
- #names = /* @__PURE__ */ new Set();
1863
- /** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
1864
- get size() {
1865
- return this.#transformers.length;
1866
- }
1867
- /** Имена опций запроса, заявленные плагинами. */
1868
- get optionKeys() {
1869
- return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
1870
- }
1871
- /**
1872
- * Подключает плагин.
1873
- *
1874
- * @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
1875
- * имя опции
1876
- */
1877
- add(plugin, context) {
1878
- validatePluginDefinition(plugin);
1879
- const name = plugin.name;
1880
- if (this.#names.has(name)) {
1881
- throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
1882
- }
1883
- const keys = plugin.optionKeys ?? [];
1884
- const before = this.#transformers.length;
1885
- try {
1886
- plugin.install({
1887
- ...context,
1888
- use: (transformer) => {
1889
- if (typeof transformer !== "function") {
1890
- throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
1891
- }
1892
- this.#transformers.push(transformer);
1893
- }
1894
- });
1895
- } catch (error) {
1896
- this.#transformers.length = before;
1897
- throw error;
1898
- }
1899
- this.#names.add(name);
1900
- for (const key of keys) this.#optionKeys.add(key);
1901
- }
1902
- /**
1903
- * Прогоняет запрос через цепочку обёрток.
1904
- *
1905
- * Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
1906
- * а обёрток единицы — экономить тут не на чем.
1907
- *
1908
- * @param execute настоящий запрос, вызывается самой внутренней обёрткой
1909
- */
1910
- run(request, execute) {
1911
- const chain = this.#transformers.reduceRight(
1912
- (next, transformer) => (current) => transformer(current, next),
1913
- execute
1914
- );
1915
- return chain(request);
1916
- }
1917
- };
1918
-
1919
2237
  // src/core/rate-limit.ts
1920
2238
  function queueAbortError() {
1921
2239
  return new ItdAbortError("\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u043E\u0447\u0435\u0440\u0435\u0434\u0438");
@@ -2513,7 +2831,7 @@ var Transport = class {
2513
2831
  }
2514
2832
  }
2515
2833
  const context = { method, path: request.path, url, headers, attempt };
2516
- await this.#config.hooks.onRequest?.(context);
2834
+ await dispatchRequestHook(this.#config.hooks, "onRequest", context, request);
2517
2835
  const timeout = request.timeout ?? this.#config.timeout;
2518
2836
  const abort = createAbortBundle(request.signal, timeout);
2519
2837
  const startedAt = Date.now();
@@ -2534,7 +2852,12 @@ var Transport = class {
2534
2852
  } catch (error) {
2535
2853
  const duration2 = Date.now() - startedAt;
2536
2854
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2537
- await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
2855
+ await dispatchRequestHook(
2856
+ this.#config.hooks,
2857
+ "onError",
2858
+ { ...context, duration: duration2, error: failure },
2859
+ request
2860
+ );
2538
2861
  this.#config.logger?.warn(
2539
2862
  `\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`
2540
2863
  );
@@ -2546,12 +2869,17 @@ var Transport = class {
2546
2869
  }
2547
2870
  if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2548
2871
  if (response.ok) {
2549
- await this.#config.hooks.onResponse?.({
2550
- ...context,
2551
- status: response.status,
2552
- duration: Date.now() - startedAt,
2553
- response
2554
- });
2872
+ await dispatchRequestHook(
2873
+ this.#config.hooks,
2874
+ "onResponse",
2875
+ {
2876
+ ...context,
2877
+ status: response.status,
2878
+ duration: Date.now() - startedAt,
2879
+ response
2880
+ },
2881
+ request
2882
+ );
2555
2883
  }
2556
2884
  const payload = await this.#readBodyOrFail(
2557
2885
  response,
@@ -2572,7 +2900,12 @@ var Transport = class {
2572
2900
  response,
2573
2901
  body: payload
2574
2902
  });
2575
- await this.#config.hooks.onError?.({ ...context, duration, error });
2903
+ await dispatchRequestHook(
2904
+ this.#config.hooks,
2905
+ "onError",
2906
+ { ...context, duration, error },
2907
+ request
2908
+ );
2576
2909
  this.#config.logger?.warn(
2577
2910
  `\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
2578
2911
  );
@@ -2593,11 +2926,16 @@ var Transport = class {
2593
2926
  await response.body?.cancel().catch(() => {
2594
2927
  });
2595
2928
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2596
- await this.#config.hooks.onError?.({
2597
- ...context,
2598
- duration: Date.now() - startedAt,
2599
- error: failure
2600
- });
2929
+ await dispatchRequestHook(
2930
+ this.#config.hooks,
2931
+ "onError",
2932
+ {
2933
+ ...context,
2934
+ duration: Date.now() - startedAt,
2935
+ error: failure
2936
+ },
2937
+ request
2938
+ );
2601
2939
  this.#config.logger?.warn(
2602
2940
  `\xD7 ${method} ${request.path}: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0442\u0435\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u0430 \u2014 ${failure.message}`
2603
2941
  );
@@ -2648,12 +2986,16 @@ var Transport = class {
2648
2986
  }
2649
2987
  /** Превращает исключение `fetch` в понятную ошибку библиотеки. */
2650
2988
  #toTransportError(error, abort, request, method, timeout) {
2651
- const aborted = error instanceof Error && error.name === "AbortError";
2989
+ const aborted = abort.signal.aborted || error instanceof Error && error.name === "AbortError";
2652
2990
  if (aborted && abort.timedOut()) {
2653
2991
  return new ItdTimeoutError({ timeout, method, path: request.path });
2654
2992
  }
2655
2993
  if (aborted) {
2656
- return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
2994
+ const reason = request.signal?.reason;
2995
+ return new ItdAbortError(
2996
+ `\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`,
2997
+ reason !== void 0 ? { cause: reason } : void 0
2998
+ );
2657
2999
  }
2658
3000
  return new ItdNetworkError(
2659
3001
  `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C ${method} ${request.path}: ${error instanceof Error ? error.message : String(error)}`,
@@ -2951,7 +3293,6 @@ var PollTransport = class {
2951
3293
  const seen = /* @__PURE__ */ new Set();
2952
3294
  let firstRun = true;
2953
3295
  let lastUnreadCount;
2954
- context.onOpen();
2955
3296
  while (!context.signal.aborted) {
2956
3297
  const token = await context.getToken();
2957
3298
  if (!token) throw new UnauthorizedStreamError();
@@ -2966,6 +3307,7 @@ var PollTransport = class {
2966
3307
  });
2967
3308
  if (response.status === 401) throw new UnauthorizedStreamError();
2968
3309
  if (!response.ok) throw new Error(`\u041E\u043F\u0440\u043E\u0441 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0442\u0430\u0442\u0443\u0441 ${response.status}`);
3310
+ context.onOpen();
2969
3311
  const body = await response.json();
2970
3312
  const payload = typeof body === "object" && body !== null && "data" in body ? body.data : body;
2971
3313
  const items = pickArray(payload, "notifications");
@@ -3211,10 +3553,12 @@ var STREAM_PATH = "/api/notifications/stream";
3211
3553
  var SseTransport = class {
3212
3554
  name = "sse";
3213
3555
  #idleTimeout;
3556
+ #handshakeTimeout;
3214
3557
  /** Идентификатор последнего события — отправляется при переподключении. */
3215
3558
  #lastEventId;
3216
3559
  constructor(options = {}) {
3217
3560
  this.#idleTimeout = options.idleTimeout ?? 9e4;
3561
+ this.#handshakeTimeout = options.handshakeTimeout ?? 2e4;
3218
3562
  }
3219
3563
  async connect(context) {
3220
3564
  const token = await context.getToken();
@@ -3225,16 +3569,36 @@ var SseTransport = class {
3225
3569
  headers.set("Authorization", `Bearer ${token}`);
3226
3570
  headers.set("Cache-Control", "no-cache");
3227
3571
  if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
3228
- const response = await context.fetch(url, {
3229
- method: "GET",
3230
- headers,
3231
- signal: context.signal
3232
- });
3233
- if (response.status === 401) throw new UnauthorizedStreamError();
3234
- if (!response.ok) throw new Error(`\u041F\u043E\u0442\u043E\u043A \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0442\u0430\u0442\u0443\u0441 ${response.status}`);
3235
- if (!response.body) throw new Error("\u041E\u0442\u0432\u0435\u0442 \u043F\u043E\u0442\u043E\u043A\u0430 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u043F\u0443\u0441\u0442");
3236
- context.onOpen();
3237
- await this.#read(response.body, context);
3572
+ const controller = new AbortController();
3573
+ const relayAbort = () => controller.abort(context.signal.reason);
3574
+ if (context.signal.aborted) controller.abort(context.signal.reason);
3575
+ else context.signal.addEventListener("abort", relayAbort, { once: true });
3576
+ try {
3577
+ const response = await this.#handshake(url, headers, context, controller);
3578
+ if (response.status === 401) throw new UnauthorizedStreamError();
3579
+ if (!response.ok) throw new Error(`\u041F\u043E\u0442\u043E\u043A \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0442\u0430\u0442\u0443\u0441 ${response.status}`);
3580
+ if (!response.body) throw new Error("\u041E\u0442\u0432\u0435\u0442 \u043F\u043E\u0442\u043E\u043A\u0430 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u043F\u0443\u0441\u0442");
3581
+ context.onOpen();
3582
+ await this.#read(response.body, context);
3583
+ } finally {
3584
+ context.signal.removeEventListener("abort", relayAbort);
3585
+ }
3586
+ }
3587
+ /** Выполняет запрос потока, обрывая его, если ответ не пришёл за отведённое время. */
3588
+ async #handshake(url, headers, context, controller) {
3589
+ let expired = false;
3590
+ const timer = this.#handshakeTimeout > 0 ? setTimeout(() => {
3591
+ expired = true;
3592
+ controller.abort(new Error("\u0418\u0441\u0442\u0451\u043A \u0442\u0430\u0439\u043C\u0430\u0443\u0442 \u0440\u0443\u043A\u043E\u043F\u043E\u0436\u0430\u0442\u0438\u044F SSE"));
3593
+ }, this.#handshakeTimeout) : void 0;
3594
+ try {
3595
+ return await context.fetch(url, { method: "GET", headers, signal: controller.signal });
3596
+ } catch (error) {
3597
+ if (expired) throw new Error("\u041F\u043E\u0442\u043E\u043A \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B: \u0438\u0441\u0442\u0451\u043A \u0442\u0430\u0439\u043C\u0430\u0443\u0442 \u0440\u0443\u043A\u043E\u043F\u043E\u0436\u0430\u0442\u0438\u044F");
3598
+ throw error;
3599
+ } finally {
3600
+ if (timer !== void 0) clearTimeout(timer);
3601
+ }
3238
3602
  }
3239
3603
  async #read(body, context) {
3240
3604
  const reader = body.getReader();
@@ -3306,6 +3670,7 @@ function validateRealtimeOptions(options) {
3306
3670
  positiveInteger(options.maxAttempts, "maxAttempts");
3307
3671
  duration(options.pollInterval, "pollInterval", 1);
3308
3672
  duration(options.idleTimeout, "idleTimeout", 0);
3673
+ duration(options.handshakeTimeout, "handshakeTimeout", 0);
3309
3674
  if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) {
3310
3675
  throw new ItdConfigError(
3311
3676
  `realtime.jitter \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0432 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0435 0\u20261, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${options.jitter}`
@@ -3427,7 +3792,8 @@ var ItdRealtime = class {
3427
3792
  });
3428
3793
  }
3429
3794
  return new SseTransport({
3430
- ...this.#options.idleTimeout !== void 0 ? { idleTimeout: this.#options.idleTimeout } : {}
3795
+ ...this.#options.idleTimeout !== void 0 ? { idleTimeout: this.#options.idleTimeout } : {},
3796
+ ...this.#options.handshakeTimeout !== void 0 ? { handshakeTimeout: this.#options.handshakeTimeout } : {}
3431
3797
  });
3432
3798
  }
3433
3799
  /** Запускает попытку подключения; повторы планирует сам. */
@@ -3642,6 +4008,11 @@ var Paginator = class {
3642
4008
  #finished = false;
3643
4009
  #pagesLoaded = 0;
3644
4010
  constructor(options) {
4011
+ if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages < 1)) {
4012
+ throw new ItdConfigError(
4013
+ `maxPages \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 1, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${options.maxPages}`
4014
+ );
4015
+ }
3645
4016
  this.#options = options;
3646
4017
  this.#maxPages = options.maxPages ?? 1e3;
3647
4018
  this.#state = options.start ?? {};
@@ -3691,6 +4062,7 @@ var Paginator = class {
3691
4062
  * @param max сколько элементов достаточно; без него перебираются все страницы
3692
4063
  */
3693
4064
  async collect(max) {
4065
+ if (max !== void 0 && max <= 0) return [];
3694
4066
  const result = [];
3695
4067
  for await (const item of this) {
3696
4068
  result.push(item);
@@ -6166,6 +6538,13 @@ var VerificationResource = class extends BaseResource {
6166
6538
  };
6167
6539
 
6168
6540
  // src/client.ts
6541
+ var CLIENT_PLUGIN_REGISTRIES = /* @__PURE__ */ new WeakMap();
6542
+ function assertClientCanUsePlugin(client, plugin) {
6543
+ CLIENT_PLUGIN_REGISTRIES.get(client)?.assertCanAdd(plugin);
6544
+ }
6545
+ function assertClientCanUnusePlugin(client, name) {
6546
+ CLIENT_PLUGIN_REGISTRIES.get(client)?.assertCanRemove(name);
6547
+ }
6169
6548
  var ItdClient = class _ItdClient {
6170
6549
  #config;
6171
6550
  #http;
@@ -6203,13 +6582,10 @@ var ItdClient = class _ItdClient {
6203
6582
  subscription;
6204
6583
  /** Сведения о платформе: изменения, анонсы, баннер события. */
6205
6584
  platform;
6206
- /**
6207
- * Телеметрия просмотров.
6208
- *
6209
- * @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
6210
- */
6585
+ /** Телеметрия просмотров. */
6211
6586
  telemetry;
6212
6587
  constructor(options = {}, internals = {}) {
6588
+ CLIENT_PLUGIN_REGISTRIES.set(this, this.#plugins);
6213
6589
  const config = resolveConfig(options);
6214
6590
  this.#config = config;
6215
6591
  this.#jar = new CookieJar();
@@ -6221,18 +6597,22 @@ var ItdClient = class _ItdClient {
6221
6597
  this.#queues = queues;
6222
6598
  this.#ownsQueues = shared === void 0;
6223
6599
  let authManager;
6224
- const transport = new Transport(config, {
6225
- cookies: config.useCookieJar ? this.#jar : void 0,
6226
- getDeviceId: () => authManager.getDeviceId(),
6227
- onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
6228
- });
6600
+ const hooks = this.#plugins.hooks(config.hooks);
6601
+ const transport = new Transport(
6602
+ { ...config, hooks },
6603
+ {
6604
+ cookies: config.useCookieJar ? this.#jar : void 0,
6605
+ getDeviceId: () => authManager.getDeviceId(),
6606
+ onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
6607
+ }
6608
+ );
6229
6609
  this.#transport = transport;
6230
6610
  const pluginsLayer = createPluginsMiddleware(this.#plugins);
6231
6611
  const retriesLayer = createRetryMiddleware({
6232
6612
  retry: config.retry,
6233
6613
  rateLimitDelays: config.rateLimit?.retryDelays ?? [],
6234
6614
  pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
6235
- hooks: config.hooks,
6615
+ hooks,
6236
6616
  logger: config.logger,
6237
6617
  buildUrl: (request) => transport.buildUrl(request)
6238
6618
  });
@@ -6332,6 +6712,26 @@ var ItdClient = class _ItdClient {
6332
6712
  });
6333
6713
  return this;
6334
6714
  }
6715
+ /** Имена подключённых плагинов в фактическом порядке выполнения обёрток. */
6716
+ pluginNames() {
6717
+ return this.#plugins.names();
6718
+ }
6719
+ /** Подключён ли плагин с таким именем. */
6720
+ hasPlugin(name) {
6721
+ return this.#plugins.has(name);
6722
+ }
6723
+ /**
6724
+ * Отключает плагин и освобождает заведённые им ресурсы.
6725
+ *
6726
+ * Новые запросы перестают видеть плагин сразу. Очистка дождётся логического запроса,
6727
+ * который уже проходил через его обёртку.
6728
+ *
6729
+ * @returns `false`, если такого плагина не было
6730
+ * @throws {ItdConfigError} если от плагина зависит другой подключённый плагин
6731
+ */
6732
+ unuse(name) {
6733
+ return this.#plugins.remove(name);
6734
+ }
6335
6735
  /**
6336
6736
  * Регистрирует сервис платформы — домен, отличный от основного.
6337
6737
  *
@@ -6437,13 +6837,25 @@ var ItdClient = class _ItdClient {
6437
6837
  * ```ts
6438
6838
  * await using itd = new ItdClient({ auth: token });
6439
6839
  * // …работа…
6440
- * // close() вызовется сам на выходе из блока
6840
+ * // dispose() вызовется сам на выходе из блока
6441
6841
  * ```
6442
6842
  */
6443
6843
  async close() {
6444
6844
  this.#disconnectStreams();
6445
6845
  if (this.#ownsQueues) this.#queues?.stop();
6446
6846
  }
6847
+ /**
6848
+ * Окончательно освобождает клиент: выполняет {@link close} и отключает все плагины.
6849
+ *
6850
+ * В отличие от `close()`, после `dispose()` плагины не восстанавливаются автоматически.
6851
+ * Сам клиент остаётся пригоден для обычных запросов; при необходимости плагины можно
6852
+ * подключить заново через {@link use}.
6853
+ */
6854
+ async dispose() {
6855
+ const results = await Promise.allSettled([this.close(), this.#plugins.dispose()]);
6856
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
6857
+ if (errors.length > 0) throw new AggregateError(errors, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u044C \u043A\u043B\u0438\u0435\u043D\u0442");
6858
+ }
6447
6859
  /** Завершает потоки до того, как запросы начнут использовать другой аккаунт. */
6448
6860
  #disconnectStreams() {
6449
6861
  for (const stream of [...this.#streams]) stream.disconnect();
@@ -6451,7 +6863,7 @@ var ItdClient = class _ItdClient {
6451
6863
  }
6452
6864
  /** Позволяет использовать клиент с `await using`. */
6453
6865
  [Symbol.asyncDispose]() {
6454
- return this.close();
6866
+ return this.dispose();
6455
6867
  }
6456
6868
  static {
6457
6869
  if (typeof Symbol.asyncDispose !== "symbol") {
@@ -6650,10 +7062,13 @@ var ItdAccounts = class _ItdAccounts {
6650
7062
  #eventUnsubscribers = /* @__PURE__ */ new Map();
6651
7063
  /** Плагины для всех: и для уже заведённых аккаунтов, и для будущих. */
6652
7064
  #plugins;
7065
+ /** Имена плагинов, чья асинхронная очистка ещё не завершилась. */
7066
+ #removingPlugins = /* @__PURE__ */ new Set();
6653
7067
  /** Общая очередь. `undefined`, когда у каждого аккаунта своя. */
6654
7068
  #queues;
6655
7069
  #rateLimitScope;
6656
7070
  #emitter;
7071
+ #logger;
6657
7072
  #createClient;
6658
7073
  constructor(options = {}, internals = {}) {
6659
7074
  const { storage, plugins, rateLimitScope, ...base } = options;
@@ -6662,19 +7077,13 @@ var ItdAccounts = class _ItdAccounts {
6662
7077
  }
6663
7078
  this.#base = base;
6664
7079
  this.#storage = storage ?? new MemoryMultiTokenStorage();
6665
- this.#plugins = [];
6666
- for (const plugin of plugins ?? []) {
6667
- validatePluginDefinition(plugin);
6668
- if (this.#plugins.some((added) => added.name === plugin.name)) {
6669
- throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
6670
- }
6671
- this.#plugins.push(plugin);
6672
- }
7080
+ this.#plugins = orderPluginDefinitions(plugins ?? []);
6673
7081
  this.#rateLimitScope = rateLimitScope ?? "account";
6674
7082
  this.#createClient = internals.createClient ?? ((clientOptions, clientInternals) => new ItdClient(clientOptions, clientInternals));
6675
7083
  const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
6676
7084
  this.#queues = rateLimit ? new RequestQueuePool(rateLimit) : void 0;
6677
7085
  const logger = typeof base.logger === "object" ? base.logger : void 0;
7086
+ this.#logger = logger;
6678
7087
  this.#emitter = new Emitter(
6679
7088
  (error) => reportListenerError(logger, "\u0430\u043A\u043A\u0430\u0443\u043D\u0442\u043E\u0432", error)
6680
7089
  );
@@ -6740,6 +7149,11 @@ var ItdAccounts = class _ItdAccounts {
6740
7149
  for (const plugin of this.#plugins) client.use(plugin);
6741
7150
  } catch (error) {
6742
7151
  storageControl.revoke();
7152
+ if (client) {
7153
+ void client.dispose().catch((cleanupError) => {
7154
+ this.#reportPluginCleanup("\u043D\u0435\u0443\u0434\u0430\u0447\u043D\u043E\u0433\u043E \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430", cleanupError);
7155
+ });
7156
+ }
6743
7157
  throw error;
6744
7158
  }
6745
7159
  const unsubscribers = this.#forwardEvents(name, client);
@@ -6821,7 +7235,7 @@ var ItdAccounts = class _ItdAccounts {
6821
7235
  try {
6822
7236
  const errors = [];
6823
7237
  const closing = await Promise.allSettled([
6824
- client.close(),
7238
+ client.dispose(),
6825
7239
  storageControl?.drain() ?? Promise.resolve()
6826
7240
  ]);
6827
7241
  for (const result of closing) {
@@ -6849,14 +7263,64 @@ var ItdAccounts = class _ItdAccounts {
6849
7263
  * ```
6850
7264
  */
6851
7265
  use(plugin) {
6852
- validatePluginDefinition(plugin);
6853
- if (this.#plugins.some((added) => added.name === plugin?.name)) {
6854
- throw new ItdConfigError(`\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
7266
+ if (this.#removingPlugins.has(plugin?.name)) {
7267
+ throw new ItdConfigError(
7268
+ `\u043F\u043B\u0430\u0433\u0438\u043D \xAB${plugin.name}\xBB \u0435\u0449\u0451 \u043E\u0442\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F; \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F accounts.unuse()`
7269
+ );
6855
7270
  }
6856
- for (const client of this.#clients.values()) client.use(plugin);
6857
- this.#plugins.push(plugin);
7271
+ const ordered = orderPluginDefinitions([...this.#plugins, plugin]);
7272
+ const clients = [...this.#clients.values()];
7273
+ for (const client of clients) assertClientCanUsePlugin(client, plugin);
7274
+ const installed = [];
7275
+ try {
7276
+ for (const client of clients) {
7277
+ client.use(plugin);
7278
+ installed.push(client);
7279
+ }
7280
+ } catch (error) {
7281
+ for (const client of installed.reverse()) {
7282
+ void client.unuse(plugin.name).catch((cleanupError) => {
7283
+ this.#reportPluginCleanup(`\u043E\u0442\u043A\u0430\u0442\u0430 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \xAB${plugin.name}\xBB`, cleanupError);
7284
+ });
7285
+ }
7286
+ throw error;
7287
+ }
7288
+ this.#plugins.splice(0, this.#plugins.length, ...ordered);
6858
7289
  return this;
6859
7290
  }
7291
+ /** Имена общих плагинов в фактическом порядке выполнения обёрток. */
7292
+ pluginNames() {
7293
+ return this.#plugins.map((plugin) => plugin.name);
7294
+ }
7295
+ /** Подключён ли общий плагин с таким именем. */
7296
+ hasPlugin(name) {
7297
+ return this.#plugins.some((plugin) => plugin.name === name);
7298
+ }
7299
+ /**
7300
+ * Отключает общий плагин у существующих клиентов и не применяет его к будущим.
7301
+ *
7302
+ * @returns `false`, если такого плагина не было
7303
+ * @throws {ItdConfigError} если от плагина зависит другой общий плагин
7304
+ */
7305
+ async unuse(name) {
7306
+ const index = this.#plugins.findIndex((plugin) => plugin.name === name);
7307
+ if (index < 0) return false;
7308
+ assertPluginRemovable(this.#plugins, name);
7309
+ const clients = [...this.#clients.values()];
7310
+ for (const client of clients) assertClientCanUnusePlugin(client, name);
7311
+ this.#plugins.splice(index, 1);
7312
+ this.#removingPlugins.add(name);
7313
+ try {
7314
+ const results = await Promise.allSettled(clients.map((client) => client.unuse(name)));
7315
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
7316
+ if (errors.length > 0) {
7317
+ throw new AggregateError(errors, `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443 \u0432\u0441\u0435\u0445 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u043E\u0432`);
7318
+ }
7319
+ return true;
7320
+ } finally {
7321
+ this.#removingPlugins.delete(name);
7322
+ }
7323
+ }
6860
7324
  /**
6861
7325
  * Подписывается на события авторизации всех аккаунтов сразу.
6862
7326
  *
@@ -6894,16 +7358,32 @@ var ItdAccounts = class _ItdAccounts {
6894
7358
  * ```ts
6895
7359
  * await using accounts = new ItdAccounts({ storage });
6896
7360
  * // …работа…
6897
- * // close() вызовется сам на выходе из блока
7361
+ * // dispose() вызовется сам на выходе из блока
6898
7362
  * ```
6899
7363
  */
6900
7364
  async close() {
6901
7365
  await Promise.all([...this.#clients.values()].map((client) => client.close()));
6902
7366
  this.#queues?.stop();
6903
7367
  }
7368
+ /**
7369
+ * Окончательно освобождает контейнер и отключает общие плагины у всех аккаунтов.
7370
+ *
7371
+ * Для временной остановки потоков и очереди без отключения плагинов используйте
7372
+ * {@link close}.
7373
+ */
7374
+ async dispose() {
7375
+ const results = await Promise.allSettled([
7376
+ ...[...this.#clients.values()].map((client) => client.dispose()),
7377
+ Promise.resolve().then(() => this.#queues?.stop())
7378
+ ]);
7379
+ this.#plugins.splice(0);
7380
+ this.#removingPlugins.clear();
7381
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
7382
+ if (errors.length > 0) throw new AggregateError(errors, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u044C \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u044B");
7383
+ }
6904
7384
  /** Позволяет использовать контейнер с `await using`. */
6905
7385
  [Symbol.asyncDispose]() {
6906
- return this.close();
7386
+ return this.dispose();
6907
7387
  }
6908
7388
  static {
6909
7389
  if (typeof Symbol.asyncDispose !== "symbol") {
@@ -6929,6 +7409,12 @@ var ItdAccounts = class _ItdAccounts {
6929
7409
  storage
6930
7410
  };
6931
7411
  }
7412
+ /** Не теряет ошибку фонового teardown, который синхронный API не может await-нуть. */
7413
+ #reportPluginCleanup(scope, error) {
7414
+ const message = `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C teardown \u043F\u043E\u0441\u043B\u0435 ${scope}`;
7415
+ if (this.#logger) this.#logger.error(message, error);
7416
+ else console.error(`[itd-api] ${message}`, error);
7417
+ }
6932
7418
  /** Ретранслирует события клиента наружу, добавляя к ним имя аккаунта. */
6933
7419
  #forwardEvents(account, client) {
6934
7420
  return [
@@ -7383,5 +7869,5 @@ exports.scopedTokenStorage = scopedTokenStorage;
7383
7869
  exports.statusDays = statusDays;
7384
7870
  exports.toDate = toDate;
7385
7871
  exports.utcStampToIso = utcStampToIso;
7386
- //# sourceMappingURL=chunk-QD4UHJFF.cjs.map
7387
- //# sourceMappingURL=chunk-QD4UHJFF.cjs.map
7872
+ //# sourceMappingURL=chunk-73CISRBG.cjs.map
7873
+ //# sourceMappingURL=chunk-73CISRBG.cjs.map