itd-api 0.0.10 → 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-SMV7TF5P.js → chunk-6FB4HTKH.js} +768 -194
  3. package/dist/chunk-6FB4HTKH.js.map +1 -0
  4. package/dist/{chunk-BN4AC3DP.cjs → chunk-73CISRBG.cjs} +768 -194
  5. package/dist/chunk-73CISRBG.cjs.map +1 -0
  6. package/dist/{index-CSjDNGCE.d.cts → index-BZF4K90s.d.cts} +169 -29
  7. package/dist/{index-CSjDNGCE.d.ts → index-BZF4K90s.d.ts} +169 -29
  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-BN4AC3DP.cjs.map +0 -1
  41. package/dist/chunk-SMV7TF5P.js.map +0 -1
@@ -619,8 +619,8 @@ var ItdTimeoutError = class extends ItdError {
619
619
  }
620
620
  };
621
621
  var ItdAbortError = class extends ItdError {
622
- constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D") {
623
- super(ItdErrorKind.Abort, message);
622
+ constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D", options) {
623
+ super(ItdErrorKind.Abort, message, options);
624
624
  this.name = "ItdAbortError";
625
625
  }
626
626
  };
@@ -672,18 +672,24 @@ function decodeBase64Url(segment) {
672
672
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
673
673
  return new TextDecoder().decode(bytes);
674
674
  }
675
- function readTokenSubject(token) {
675
+ function readTokenIdentity(token) {
676
676
  try {
677
677
  const payload = token.split(".")[1];
678
- if (!payload) return void 0;
678
+ if (!payload) return {};
679
679
  const parsed = JSON.parse(decodeBase64Url(payload));
680
- if (typeof parsed !== "object" || parsed === null) return void 0;
681
- const subject = parsed.sub;
682
- return typeof subject === "string" && subject.length > 0 ? subject : void 0;
680
+ if (typeof parsed !== "object" || parsed === null) return {};
681
+ const { sub, sid } = parsed;
682
+ return {
683
+ ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
684
+ ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {}
685
+ };
683
686
  } catch {
684
- return void 0;
687
+ return {};
685
688
  }
686
689
  }
690
+ function readTokenSubject(token) {
691
+ return readTokenIdentity(token).subject;
692
+ }
687
693
 
688
694
  // src/core/runtime.ts
689
695
  var RuntimeMode = Object.freeze({
@@ -876,6 +882,7 @@ var AuthManager = class {
876
882
  #send;
877
883
  #jar;
878
884
  #emitter;
885
+ #hooks;
879
886
  /** `undefined` — сессия ещё не читалась из хранилища. */
880
887
  #session;
881
888
  /**
@@ -887,8 +894,12 @@ var AuthManager = class {
887
894
  #refreshing = null;
888
895
  /** Общий промис входа по логину и паролю. */
889
896
  #signingIn = null;
890
- /** Непрозрачная версия владельца клиента для разделения состояния плагинов. */
897
+ /** Fallback-область для изоляции состояния плагинов, когда JWT-идентичность недоступна. */
891
898
  #authScope = nextAuthScope();
899
+ /** Последний токен внешнего источника; `undefined` означает, что источник ещё не читался. */
900
+ #externalToken;
901
+ /** Идентификаторы последнего токена внешнего источника для синхронных потребителей. */
902
+ #externalIdentity;
892
903
  /**
893
904
  * Идентификатор устройства.
894
905
  *
@@ -898,10 +909,20 @@ var AuthManager = class {
898
909
  #deviceId;
899
910
  /** Общий промис первичной выдачи `deviceId` — чтобы параллельные запросы получили один. */
900
911
  #deviceIdLoading = null;
901
- constructor(config, send, jar) {
912
+ /**
913
+ * Счётчик смен владельца авторизации.
914
+ *
915
+ * Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
916
+ * `setSession`, `setAccessToken`, вход. `#performRefresh` снимает его значение перед
917
+ * сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
918
+ * воскресить уже очищенную сессию поверх `signOut`.
919
+ */
920
+ #authEpoch = 0;
921
+ constructor(config, send, jar, hooks = {}) {
902
922
  this.#config = config;
903
923
  this.#send = send;
904
924
  this.#jar = jar;
925
+ this.#hooks = hooks;
905
926
  this.#emitter = new Emitter(
906
927
  (error) => reportListenerError(config.logger, "\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", error)
907
928
  );
@@ -914,13 +935,67 @@ var AuthManager = class {
914
935
  get once() {
915
936
  return this.#emitter.once.bind(this.#emitter);
916
937
  }
917
- /** Непрозрачная область авторизации; токен и идентификатор пользователя не раскрываются. */
938
+ /**
939
+ * Непрозрачная fallback-область авторизации.
940
+ *
941
+ * Изолирует состояние плагинов, когда идентичность аккаунта из JWT недоступна (непрозрачный
942
+ * токен): значение уникально для владельца и сменяется при замене или очистке сессии.
943
+ */
918
944
  getAuthScope() {
919
945
  return this.#authScope;
920
946
  }
947
+ /** Загружает сессию и возвращает идентификаторы аккаунта и сессии для плагинов. */
948
+ async getAuthIdentity() {
949
+ const session = await this.#loadSession();
950
+ if (session?.accessToken) return this.#identityForToken(session.accessToken);
951
+ const auth = this.#config.auth;
952
+ if (typeof auth === "object" && auth !== null && "getToken" in auth) {
953
+ const token = await this.#readExternalToken(() => auth.getToken());
954
+ return this.#identityForToken(token ?? void 0);
955
+ }
956
+ return {};
957
+ }
958
+ /** Идентификаторы аккаунта и сессии без чтения хранилища. @internal */
959
+ getCurrentAuthIdentity() {
960
+ const session = this.#session === void 0 ? this.#sessionFromConfig(this.#config.auth) : this.#session;
961
+ return session?.accessToken ? this.#identityForToken(session.accessToken) : this.#externalIdentity ?? {};
962
+ }
921
963
  #rotateAuthScope() {
922
964
  this.#authScope = nextAuthScope();
923
965
  }
966
+ /** Отмечает смену владельца авторизации — обесценивает результат идущего обновления. */
967
+ #invalidateInFlight() {
968
+ this.#authEpoch += 1;
969
+ }
970
+ #identityForToken(accessToken) {
971
+ const token = accessToken ? readTokenIdentity(accessToken) : {};
972
+ return {
973
+ ...token.subject ? { userId: token.subject } : {},
974
+ ...token.sessionId ? { sessionId: token.sessionId } : {}
975
+ };
976
+ }
977
+ async #readExternalToken(getToken) {
978
+ const token = await getToken() ?? null;
979
+ const current = this.#identityForToken(token ?? void 0);
980
+ if (this.#externalToken !== void 0 && this.#externalToken !== token) {
981
+ this.#rotateAuthScope();
982
+ const previous = this.#externalIdentity ?? {};
983
+ const changed = previous.userId !== void 0 && current.userId !== void 0 ? previous.userId !== current.userId : true;
984
+ if (changed) this.#hooks.onAccountChange?.();
985
+ }
986
+ this.#externalToken = token;
987
+ this.#externalIdentity = current;
988
+ return token;
989
+ }
990
+ /** Меняет fallback и завершает realtime, только если фактически сменился аккаунт. */
991
+ #transitionAuth(accessToken, rotateFallback = true) {
992
+ const knownPrevious = this.#session !== void 0;
993
+ const previous = this.#identityForToken(this.#session?.accessToken);
994
+ const current = this.#identityForToken(accessToken);
995
+ const changed = previous.userId !== void 0 && current.userId !== void 0 ? previous.userId !== current.userId : previous.userId !== current.userId || rotateFallback;
996
+ if (rotateFallback || changed) this.#rotateAuthScope();
997
+ if (knownPrevious && changed) this.#hooks.onAccountChange?.();
998
+ }
924
999
  /**
925
1000
  * Есть ли признак живой refresh-сессии.
926
1001
  *
@@ -981,7 +1056,7 @@ var AuthManager = class {
981
1056
  const auth = this.#config.auth;
982
1057
  if (!auth) return null;
983
1058
  if (typeof auth === "object" && "getToken" in auth) {
984
- return await auth.getToken() ?? null;
1059
+ return this.#readExternalToken(() => auth.getToken());
985
1060
  }
986
1061
  if (typeof auth === "object" && "email" in auth) {
987
1062
  return this.#signInWithCredentials(auth);
@@ -1036,7 +1111,9 @@ var AuthManager = class {
1036
1111
  }
1037
1112
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
1038
1113
  async setAccessToken(accessToken) {
1039
- this.#rotateAuthScope();
1114
+ await this.#loadSession();
1115
+ this.#invalidateInFlight();
1116
+ this.#transitionAuth(accessToken);
1040
1117
  await this.#saveSession({ ...this.#session ?? {}, accessToken, obtainedAt: Date.now() });
1041
1118
  this.#emitter.emit("tokens", { accessToken });
1042
1119
  }
@@ -1057,7 +1134,9 @@ var AuthManager = class {
1057
1134
  }
1058
1135
  /** Заменяет сессию и связанные с ней cookie целиком. */
1059
1136
  async setSession(session) {
1060
- this.#rotateAuthScope();
1137
+ await this.#loadSession();
1138
+ this.#invalidateInFlight();
1139
+ this.#transitionAuth(session.accessToken);
1061
1140
  this.#jar.clear();
1062
1141
  this.#jar.deserialize(session.cookies);
1063
1142
  if (session.deviceId) this.#deviceId = session.deviceId;
@@ -1072,8 +1151,10 @@ var AuthManager = class {
1072
1151
  * новую запись в списке сессий.
1073
1152
  */
1074
1153
  async clear() {
1154
+ await this.#loadSession();
1155
+ this.#invalidateInFlight();
1156
+ this.#transitionAuth(void 0);
1075
1157
  this.#session = null;
1076
- this.#rotateAuthScope();
1077
1158
  this.#jar.clear();
1078
1159
  await this.#config.storage.clear();
1079
1160
  if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
@@ -1154,6 +1235,7 @@ var AuthManager = class {
1154
1235
  if (!this.#hasRefreshSession()) {
1155
1236
  return this.#reloginOrNull();
1156
1237
  }
1238
+ const epoch = this.#authEpoch;
1157
1239
  try {
1158
1240
  const payload = await this.#send({
1159
1241
  method: "POST",
@@ -1166,10 +1248,14 @@ var AuthManager = class {
1166
1248
  });
1167
1249
  const accessToken = readAccessToken(payload);
1168
1250
  if (!accessToken) return this.#reloginOrNull();
1251
+ if (this.#authEpoch !== epoch) {
1252
+ return this.#session?.accessToken ?? null;
1253
+ }
1169
1254
  const rotated = this.#jar.getValue(
1170
1255
  REFRESH_COOKIE,
1171
1256
  this.#config.baseUrl + REFRESH_COOKIE_PATH
1172
1257
  );
1258
+ this.#transitionAuth(accessToken, false);
1173
1259
  await this.#saveSession({
1174
1260
  ...this.#session ?? {},
1175
1261
  accessToken,
@@ -1180,8 +1266,11 @@ var AuthManager = class {
1180
1266
  return accessToken;
1181
1267
  } catch (error) {
1182
1268
  if (error instanceof ItdApiError) {
1269
+ if (this.#authEpoch !== epoch) {
1270
+ return this.#session?.accessToken ?? null;
1271
+ }
1272
+ this.#transitionAuth(void 0);
1183
1273
  this.#session = null;
1184
- this.#rotateAuthScope();
1185
1274
  this.#jar.clear();
1186
1275
  await this.#config.storage.clear();
1187
1276
  const relogged = await this.#reloginOrNull();
@@ -1249,7 +1338,8 @@ var AuthManager = class {
1249
1338
  "\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."
1250
1339
  );
1251
1340
  }
1252
- this.#rotateAuthScope();
1341
+ this.#invalidateInFlight();
1342
+ this.#transitionAuth(accessToken);
1253
1343
  await this.#saveSession({ accessToken, obtainedAt: Date.now() });
1254
1344
  this.#emitter.emit("tokens", { accessToken });
1255
1345
  this.#emitter.emit("signIn", { accessToken });
@@ -1316,7 +1406,7 @@ function normalizeBaseUrl(baseUrl) {
1316
1406
  }
1317
1407
 
1318
1408
  // src/core/version.ts
1319
- var LIBRARY_VERSION = "0.0.10";
1409
+ var LIBRARY_VERSION = "0.1.0";
1320
1410
 
1321
1411
  // src/core/config.ts
1322
1412
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
@@ -1601,6 +1691,396 @@ function withLayerHeaders(request, headers) {
1601
1691
  return { ...request, layerHeaders: { ...request.layerHeaders, ...headers } };
1602
1692
  }
1603
1693
 
1694
+ // src/core/plugins.ts
1695
+ var NO_KEYS = /* @__PURE__ */ new Set();
1696
+ var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
1697
+ "signal",
1698
+ "timeout",
1699
+ "headers",
1700
+ "retry",
1701
+ "method",
1702
+ "path",
1703
+ "service",
1704
+ "baseUrl",
1705
+ "query",
1706
+ "body",
1707
+ "skipAuth",
1708
+ "skipAuthRefresh",
1709
+ "skipQueue",
1710
+ "raw"
1711
+ ]);
1712
+ var RELATION_FIELDS = ["requires", "conflicts", "before", "after"];
1713
+ var HOOK_FIELDS = ["onRequest", "onResponse", "onError", "onRetry"];
1714
+ function validateNameList(plugin, field) {
1715
+ const values = plugin[field];
1716
+ if (values === void 0) return;
1717
+ if (!Array.isArray(values)) {
1718
+ 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`);
1719
+ }
1720
+ const seen = /* @__PURE__ */ new Set();
1721
+ for (const value of values) {
1722
+ if (typeof value !== "string" || value.trim() === "") {
1723
+ 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`);
1724
+ }
1725
+ if (value === plugin.name) {
1726
+ 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}`);
1727
+ }
1728
+ if (seen.has(value)) {
1729
+ 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`);
1730
+ }
1731
+ seen.add(value);
1732
+ }
1733
+ }
1734
+ function validateHooks(plugin, hooks) {
1735
+ if (typeof hooks !== "object" || hooks === null) {
1736
+ 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`);
1737
+ }
1738
+ for (const field of HOOK_FIELDS) {
1739
+ if (hooks[field] !== void 0 && typeof hooks[field] !== "function") {
1740
+ 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`);
1741
+ }
1742
+ }
1743
+ }
1744
+ function validatePluginDefinition(plugin) {
1745
+ if (typeof plugin?.install !== "function") {
1746
+ 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()");
1747
+ }
1748
+ const name = plugin.name;
1749
+ if (typeof name !== "string" || name.trim() === "") {
1750
+ 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");
1751
+ }
1752
+ const keys = plugin.optionKeys ?? [];
1753
+ if (!Array.isArray(keys)) {
1754
+ 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`);
1755
+ }
1756
+ for (const key of keys) {
1757
+ if (typeof key !== "string" || key.trim() === "") {
1758
+ 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`);
1759
+ }
1760
+ if (RESERVED_OPTION_KEYS.has(key)) {
1761
+ throw new ItdConfigError(
1762
+ `\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(", ")}`
1763
+ );
1764
+ }
1765
+ }
1766
+ for (const field of RELATION_FIELDS) validateNameList(plugin, field);
1767
+ }
1768
+ function addEdge(from, to, edges, indegree) {
1769
+ const targets = edges.get(from);
1770
+ if (!targets || targets.has(to)) return;
1771
+ targets.add(to);
1772
+ indegree.set(to, (indegree.get(to) ?? 0) + 1);
1773
+ }
1774
+ function orderPluginDefinitions(plugins) {
1775
+ const entries = [];
1776
+ const byName = /* @__PURE__ */ new Map();
1777
+ for (const [sequence, plugin] of plugins.entries()) {
1778
+ validatePluginDefinition(plugin);
1779
+ if (byName.has(plugin.name)) {
1780
+ 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`);
1781
+ }
1782
+ const entry = { plugin, sequence };
1783
+ entries.push(entry);
1784
+ byName.set(plugin.name, entry);
1785
+ }
1786
+ for (const { plugin } of entries) {
1787
+ for (const required of plugin.requires ?? []) {
1788
+ if (!byName.has(required)) {
1789
+ 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`);
1790
+ }
1791
+ }
1792
+ for (const conflict of plugin.conflicts ?? []) {
1793
+ if (byName.has(conflict)) {
1794
+ 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`);
1795
+ }
1796
+ }
1797
+ for (const { plugin: other } of entries) {
1798
+ if (other.conflicts?.includes(plugin.name)) {
1799
+ 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`);
1800
+ }
1801
+ }
1802
+ }
1803
+ const edges = /* @__PURE__ */ new Map();
1804
+ const indegree = /* @__PURE__ */ new Map();
1805
+ for (const { plugin } of entries) {
1806
+ edges.set(plugin.name, /* @__PURE__ */ new Set());
1807
+ indegree.set(plugin.name, 0);
1808
+ }
1809
+ for (const { plugin } of entries) {
1810
+ for (const required of plugin.requires ?? []) {
1811
+ addEdge(required, plugin.name, edges, indegree);
1812
+ }
1813
+ for (const target of plugin.before ?? []) {
1814
+ if (byName.has(target)) addEdge(plugin.name, target, edges, indegree);
1815
+ }
1816
+ for (const target of plugin.after ?? []) {
1817
+ if (byName.has(target)) addEdge(target, plugin.name, edges, indegree);
1818
+ }
1819
+ }
1820
+ const ready = entries.filter(({ plugin }) => indegree.get(plugin.name) === 0);
1821
+ ready.sort((a, b) => a.sequence - b.sequence);
1822
+ const ordered = [];
1823
+ while (ready.length > 0) {
1824
+ const current = ready.shift();
1825
+ if (!current) break;
1826
+ ordered.push(current.plugin);
1827
+ for (const target of edges.get(current.plugin.name) ?? []) {
1828
+ const next = (indegree.get(target) ?? 0) - 1;
1829
+ indegree.set(target, next);
1830
+ if (next === 0) {
1831
+ const entry = byName.get(target);
1832
+ if (entry) {
1833
+ ready.push(entry);
1834
+ ready.sort((a, b) => a.sequence - b.sequence);
1835
+ }
1836
+ }
1837
+ }
1838
+ }
1839
+ if (ordered.length !== entries.length) {
1840
+ const cycle = entries.filter(({ plugin }) => (indegree.get(plugin.name) ?? 0) > 0).map(({ plugin }) => plugin.name);
1841
+ 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 ")}`);
1842
+ }
1843
+ return ordered;
1844
+ }
1845
+ function assertPluginRemovable(plugins, name) {
1846
+ const dependent = plugins.find((plugin) => plugin.requires?.includes(name));
1847
+ if (dependent) {
1848
+ throw new ItdConfigError(
1849
+ `\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`
1850
+ );
1851
+ }
1852
+ }
1853
+ var REQUEST_HOOK_DISPATCHERS = /* @__PURE__ */ new WeakMap();
1854
+ var PLUGIN_HOOK_SCOPE = /* @__PURE__ */ Symbol("itd-api.plugin-hooks");
1855
+ async function dispatchRequestHook(hooks, field, context, request) {
1856
+ const dispatcher = REQUEST_HOOK_DISPATCHERS.get(hooks);
1857
+ if (dispatcher) {
1858
+ await dispatcher(field, context, request);
1859
+ return;
1860
+ }
1861
+ const hook = hooks[field];
1862
+ await hook?.(context);
1863
+ }
1864
+ var PluginRegistry = class {
1865
+ #entries = /* @__PURE__ */ new Map();
1866
+ #optionKeys = /* @__PURE__ */ new Set();
1867
+ #removing = /* @__PURE__ */ new Set();
1868
+ #cleanups = /* @__PURE__ */ new Set();
1869
+ #ordered = [];
1870
+ /** Сколько плагинов подключено. */
1871
+ get size() {
1872
+ return this.#entries.size;
1873
+ }
1874
+ /** Имена опций активных плагинов. */
1875
+ get optionKeys() {
1876
+ return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
1877
+ }
1878
+ /** Имена плагинов в фактическом порядке выполнения. */
1879
+ names() {
1880
+ return this.#ordered.map(({ plugin }) => plugin.name);
1881
+ }
1882
+ /** Подключён ли плагин с таким именем. */
1883
+ has(name) {
1884
+ return this.#entries.has(name);
1885
+ }
1886
+ /** Проверяет добавление без вызова `install()`. @internal */
1887
+ assertCanAdd(plugin) {
1888
+ validatePluginDefinition(plugin);
1889
+ if (this.#removing.has(plugin.name)) {
1890
+ throw new ItdConfigError(
1891
+ `\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()`
1892
+ );
1893
+ }
1894
+ orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
1895
+ }
1896
+ /** Проверяет удаление без изменения реестра. @internal */
1897
+ assertCanRemove(name) {
1898
+ const entry = this.#entries.get(name);
1899
+ if (!entry) return;
1900
+ assertPluginRemovable(
1901
+ this.#ordered.map((current) => current.plugin),
1902
+ name
1903
+ );
1904
+ }
1905
+ /**
1906
+ * Подключает плагин.
1907
+ *
1908
+ * @throws {ItdConfigError} если плагин задан неверно, уже подключён, нарушает зависимости
1909
+ * или заявил занятое имя опции
1910
+ */
1911
+ add(plugin, context) {
1912
+ this.assertCanAdd(plugin);
1913
+ const ordered = orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
1914
+ const transformers = [];
1915
+ const hooks = [];
1916
+ const installed = plugin.install({
1917
+ ...context,
1918
+ use: (transformer) => {
1919
+ if (typeof transformer !== "function") {
1920
+ 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`);
1921
+ }
1922
+ transformers.push(transformer);
1923
+ },
1924
+ useHooks: (value) => {
1925
+ validateHooks(plugin.name, value);
1926
+ hooks.push({ ...value });
1927
+ }
1928
+ });
1929
+ const teardown = typeof installed === "function" ? installed : void 0;
1930
+ this.#entries.set(plugin.name, {
1931
+ plugin,
1932
+ transformers,
1933
+ hooks,
1934
+ teardown,
1935
+ activeRequests: 0,
1936
+ drain: void 0,
1937
+ finishDrain: void 0
1938
+ });
1939
+ this.#ordered = ordered.map((definition) => {
1940
+ const entry = this.#entries.get(definition.name);
1941
+ 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`);
1942
+ return entry;
1943
+ });
1944
+ this.#rebuildOptionKeys();
1945
+ }
1946
+ /**
1947
+ * Отключает плагин и вызывает его функцию очистки.
1948
+ *
1949
+ * Новые запросы перестают видеть плагин сразу. Если его обёртка уже выполняется,
1950
+ * очистка дождётся завершения этого логического запроса.
1951
+ *
1952
+ * @returns `false`, если такого плагина не было
1953
+ */
1954
+ async remove(name) {
1955
+ const entry = this.#entries.get(name);
1956
+ if (!entry) return false;
1957
+ this.assertCanRemove(name);
1958
+ this.#entries.delete(name);
1959
+ this.#ordered = this.#ordered.filter((current) => current !== entry);
1960
+ this.#rebuildOptionKeys();
1961
+ this.#removing.add(name);
1962
+ const cleanup = this.#trackCleanup(
1963
+ (async () => {
1964
+ await this.#waitForDrain(entry);
1965
+ await entry.teardown?.();
1966
+ })()
1967
+ );
1968
+ try {
1969
+ await cleanup;
1970
+ return true;
1971
+ } finally {
1972
+ this.#removing.delete(name);
1973
+ }
1974
+ }
1975
+ /**
1976
+ * Отключает все плагины окончательно.
1977
+ *
1978
+ * Очистка идёт изнутри наружу — в порядке, обратном выполнению обёрток.
1979
+ */
1980
+ async dispose() {
1981
+ const entries = [...this.#ordered].reverse();
1982
+ const previousCleanups = [...this.#cleanups];
1983
+ this.#entries.clear();
1984
+ this.#ordered = [];
1985
+ this.#optionKeys.clear();
1986
+ for (const { plugin } of entries) this.#removing.add(plugin.name);
1987
+ const cleanup = this.#trackCleanup(
1988
+ (async () => {
1989
+ const errors = [];
1990
+ const previous = await Promise.allSettled(previousCleanups);
1991
+ for (const result of previous) {
1992
+ if (result.status === "rejected") errors.push(result.reason);
1993
+ }
1994
+ for (const entry of entries) {
1995
+ try {
1996
+ await this.#waitForDrain(entry);
1997
+ await entry.teardown?.();
1998
+ } catch (error) {
1999
+ errors.push(error);
2000
+ } finally {
2001
+ this.#removing.delete(entry.plugin.name);
2002
+ }
2003
+ }
2004
+ if (errors.length > 0) {
2005
+ 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");
2006
+ }
2007
+ })()
2008
+ );
2009
+ await cleanup;
2010
+ }
2011
+ /**
2012
+ * Объединяет конструкторские хуки с хуками подключаемых плагинов.
2013
+ *
2014
+ * Возвращённый объект динамический: подключение и отключение плагина начинает действовать
2015
+ * со следующего логического запроса без пересоздания транспорта.
2016
+ */
2017
+ hooks(base) {
2018
+ const hooks = {};
2019
+ REQUEST_HOOK_DISPATCHERS.set(hooks, ((field, context, request) => this.#runHook(field, context, request, base)));
2020
+ return hooks;
2021
+ }
2022
+ /**
2023
+ * Прогоняет запрос через цепочку обёрток.
2024
+ *
2025
+ * Снимок цепочки берётся в начале: `unuse()` влияет на новые запросы, но не обрывает
2026
+ * уже выполняющийся посередине.
2027
+ *
2028
+ * @param execute настоящий запрос, вызывается самой внутренней обёрткой
2029
+ */
2030
+ async run(request, execute) {
2031
+ const entries = [...this.#ordered];
2032
+ for (const entry of entries) entry.activeRequests += 1;
2033
+ const hookScope = entries.flatMap((entry) => entry.hooks);
2034
+ const scoped = (current) => current[PLUGIN_HOOK_SCOPE] === hookScope ? current : { ...current, [PLUGIN_HOOK_SCOPE]: hookScope };
2035
+ const chain = entries.flatMap((entry) => entry.transformers).reduceRight(
2036
+ (next, transformer) => (current) => transformer(scoped(current), (prepared) => next(scoped(prepared))),
2037
+ (current) => execute(scoped(current))
2038
+ );
2039
+ try {
2040
+ return await chain(scoped(request));
2041
+ } finally {
2042
+ for (const entry of entries) {
2043
+ entry.activeRequests -= 1;
2044
+ if (entry.activeRequests === 0) {
2045
+ entry.finishDrain?.();
2046
+ entry.finishDrain = void 0;
2047
+ entry.drain = void 0;
2048
+ }
2049
+ }
2050
+ }
2051
+ }
2052
+ #rebuildOptionKeys() {
2053
+ this.#optionKeys.clear();
2054
+ for (const { plugin } of this.#ordered) {
2055
+ for (const key of plugin.optionKeys ?? []) this.#optionKeys.add(key);
2056
+ }
2057
+ }
2058
+ #waitForDrain(entry) {
2059
+ if (entry.activeRequests === 0) return Promise.resolve();
2060
+ entry.drain ??= new Promise((resolve) => {
2061
+ entry.finishDrain = resolve;
2062
+ });
2063
+ return entry.drain;
2064
+ }
2065
+ #trackCleanup(cleanup) {
2066
+ this.#cleanups.add(cleanup);
2067
+ void cleanup.then(
2068
+ () => this.#cleanups.delete(cleanup),
2069
+ () => this.#cleanups.delete(cleanup)
2070
+ );
2071
+ return cleanup;
2072
+ }
2073
+ async #runHook(field, context, request, base) {
2074
+ const baseHook = base[field];
2075
+ await baseHook?.(context);
2076
+ const scope = request[PLUGIN_HOOK_SCOPE] ?? [];
2077
+ for (const hooks of scope) {
2078
+ const hook = hooks[field];
2079
+ await hook?.(context);
2080
+ }
2081
+ }
2082
+ };
2083
+
1604
2084
  // src/core/retry.ts
1605
2085
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
1606
2086
  function isRetryable(error, method, retryWrites) {
@@ -1656,10 +2136,7 @@ function createQueueMiddleware(schedule) {
1656
2136
  return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
1657
2137
  }
1658
2138
  function createPluginsMiddleware(plugins) {
1659
- return (request, next) => {
1660
- if (plugins.size === 0) return next(request);
1661
- return plugins.run(request, next);
1662
- };
2139
+ return (request, next) => plugins.run(request, next);
1663
2140
  }
1664
2141
  function createServicesMiddleware(registry) {
1665
2142
  return async (request, next) => {
@@ -1731,16 +2208,21 @@ function createRetryMiddleware(deps) {
1731
2208
  } catch (error) {
1732
2209
  const delay = nextDelay(error, attempt, request, method, backoff);
1733
2210
  if (delay === void 0) throw error;
1734
- await deps.hooks.onRetry?.({
1735
- method,
1736
- path: request.path,
1737
- url: deps.buildUrl(request),
1738
- // Умолчания транспорта добавляются после слоя повторов и сюда не входят.
1739
- headers: new Headers({ ...request.layerHeaders, ...request.headers }),
1740
- attempt,
1741
- error,
1742
- delay
1743
- });
2211
+ await dispatchRequestHook(
2212
+ deps.hooks,
2213
+ "onRetry",
2214
+ {
2215
+ method,
2216
+ path: request.path,
2217
+ url: deps.buildUrl(request),
2218
+ // Умолчания транспорта добавляются после слоя повторов и сюда не входят.
2219
+ headers: new Headers({ ...request.layerHeaders, ...request.headers }),
2220
+ attempt,
2221
+ error,
2222
+ delay
2223
+ },
2224
+ request
2225
+ );
1744
2226
  deps.logger?.debug(
1745
2227
  `\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`
1746
2228
  );
@@ -1750,104 +2232,6 @@ function createRetryMiddleware(deps) {
1750
2232
  };
1751
2233
  }
1752
2234
 
1753
- // src/core/plugins.ts
1754
- var NO_KEYS = /* @__PURE__ */ new Set();
1755
- var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
1756
- "signal",
1757
- "timeout",
1758
- "headers",
1759
- "retry",
1760
- "method",
1761
- "path",
1762
- "service",
1763
- "baseUrl",
1764
- "query",
1765
- "body",
1766
- "skipAuth",
1767
- "skipAuthRefresh",
1768
- "skipQueue",
1769
- "raw"
1770
- ]);
1771
- function validatePluginDefinition(plugin) {
1772
- if (typeof plugin?.install !== "function") {
1773
- 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()");
1774
- }
1775
- const name = plugin.name;
1776
- if (typeof name !== "string" || name.trim() === "") {
1777
- 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");
1778
- }
1779
- const keys = plugin.optionKeys ?? [];
1780
- for (const key of keys) {
1781
- if (typeof key !== "string" || key.trim() === "") {
1782
- 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`);
1783
- }
1784
- if (RESERVED_OPTION_KEYS.has(key)) {
1785
- throw new ItdConfigError(
1786
- `\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(", ")}`
1787
- );
1788
- }
1789
- }
1790
- }
1791
- var PluginRegistry = class {
1792
- #transformers = [];
1793
- #optionKeys = /* @__PURE__ */ new Set();
1794
- #names = /* @__PURE__ */ new Set();
1795
- /** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
1796
- get size() {
1797
- return this.#transformers.length;
1798
- }
1799
- /** Имена опций запроса, заявленные плагинами. */
1800
- get optionKeys() {
1801
- return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
1802
- }
1803
- /**
1804
- * Подключает плагин.
1805
- *
1806
- * @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
1807
- * имя опции
1808
- */
1809
- add(plugin, context) {
1810
- validatePluginDefinition(plugin);
1811
- const name = plugin.name;
1812
- if (this.#names.has(name)) {
1813
- throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
1814
- }
1815
- const keys = plugin.optionKeys ?? [];
1816
- const before = this.#transformers.length;
1817
- try {
1818
- plugin.install({
1819
- ...context,
1820
- use: (transformer) => {
1821
- if (typeof transformer !== "function") {
1822
- 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`);
1823
- }
1824
- this.#transformers.push(transformer);
1825
- }
1826
- });
1827
- } catch (error) {
1828
- this.#transformers.length = before;
1829
- throw error;
1830
- }
1831
- this.#names.add(name);
1832
- for (const key of keys) this.#optionKeys.add(key);
1833
- }
1834
- /**
1835
- * Прогоняет запрос через цепочку обёрток.
1836
- *
1837
- * Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
1838
- * а обёрток единицы — экономить тут не на чем.
1839
- *
1840
- * @param execute настоящий запрос, вызывается самой внутренней обёрткой
1841
- */
1842
- run(request, execute) {
1843
- const chain = this.#transformers.reduceRight(
1844
- (next, transformer) => (current) => transformer(current, next),
1845
- execute
1846
- );
1847
- return chain(request);
1848
- }
1849
- };
1850
-
1851
2235
  // src/core/rate-limit.ts
1852
2236
  function queueAbortError() {
1853
2237
  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");
@@ -2445,7 +2829,7 @@ var Transport = class {
2445
2829
  }
2446
2830
  }
2447
2831
  const context = { method, path: request.path, url, headers, attempt };
2448
- await this.#config.hooks.onRequest?.(context);
2832
+ await dispatchRequestHook(this.#config.hooks, "onRequest", context, request);
2449
2833
  const timeout = request.timeout ?? this.#config.timeout;
2450
2834
  const abort = createAbortBundle(request.signal, timeout);
2451
2835
  const startedAt = Date.now();
@@ -2466,7 +2850,12 @@ var Transport = class {
2466
2850
  } catch (error) {
2467
2851
  const duration2 = Date.now() - startedAt;
2468
2852
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2469
- await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
2853
+ await dispatchRequestHook(
2854
+ this.#config.hooks,
2855
+ "onError",
2856
+ { ...context, duration: duration2, error: failure },
2857
+ request
2858
+ );
2470
2859
  this.#config.logger?.warn(
2471
2860
  `\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`
2472
2861
  );
@@ -2478,12 +2867,17 @@ var Transport = class {
2478
2867
  }
2479
2868
  if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2480
2869
  if (response.ok) {
2481
- await this.#config.hooks.onResponse?.({
2482
- ...context,
2483
- status: response.status,
2484
- duration: Date.now() - startedAt,
2485
- response
2486
- });
2870
+ await dispatchRequestHook(
2871
+ this.#config.hooks,
2872
+ "onResponse",
2873
+ {
2874
+ ...context,
2875
+ status: response.status,
2876
+ duration: Date.now() - startedAt,
2877
+ response
2878
+ },
2879
+ request
2880
+ );
2487
2881
  }
2488
2882
  const payload = await this.#readBodyOrFail(
2489
2883
  response,
@@ -2504,7 +2898,12 @@ var Transport = class {
2504
2898
  response,
2505
2899
  body: payload
2506
2900
  });
2507
- await this.#config.hooks.onError?.({ ...context, duration, error });
2901
+ await dispatchRequestHook(
2902
+ this.#config.hooks,
2903
+ "onError",
2904
+ { ...context, duration, error },
2905
+ request
2906
+ );
2508
2907
  this.#config.logger?.warn(
2509
2908
  `\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
2510
2909
  );
@@ -2525,11 +2924,16 @@ var Transport = class {
2525
2924
  await response.body?.cancel().catch(() => {
2526
2925
  });
2527
2926
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2528
- await this.#config.hooks.onError?.({
2529
- ...context,
2530
- duration: Date.now() - startedAt,
2531
- error: failure
2532
- });
2927
+ await dispatchRequestHook(
2928
+ this.#config.hooks,
2929
+ "onError",
2930
+ {
2931
+ ...context,
2932
+ duration: Date.now() - startedAt,
2933
+ error: failure
2934
+ },
2935
+ request
2936
+ );
2533
2937
  this.#config.logger?.warn(
2534
2938
  `\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}`
2535
2939
  );
@@ -2580,12 +2984,16 @@ var Transport = class {
2580
2984
  }
2581
2985
  /** Превращает исключение `fetch` в понятную ошибку библиотеки. */
2582
2986
  #toTransportError(error, abort, request, method, timeout) {
2583
- const aborted = error instanceof Error && error.name === "AbortError";
2987
+ const aborted = abort.signal.aborted || error instanceof Error && error.name === "AbortError";
2584
2988
  if (aborted && abort.timedOut()) {
2585
2989
  return new ItdTimeoutError({ timeout, method, path: request.path });
2586
2990
  }
2587
2991
  if (aborted) {
2588
- return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
2992
+ const reason = request.signal?.reason;
2993
+ return new ItdAbortError(
2994
+ `\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`,
2995
+ reason !== void 0 ? { cause: reason } : void 0
2996
+ );
2589
2997
  }
2590
2998
  return new ItdNetworkError(
2591
2999
  `\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)}`,
@@ -2883,7 +3291,6 @@ var PollTransport = class {
2883
3291
  const seen = /* @__PURE__ */ new Set();
2884
3292
  let firstRun = true;
2885
3293
  let lastUnreadCount;
2886
- context.onOpen();
2887
3294
  while (!context.signal.aborted) {
2888
3295
  const token = await context.getToken();
2889
3296
  if (!token) throw new UnauthorizedStreamError();
@@ -2898,6 +3305,7 @@ var PollTransport = class {
2898
3305
  });
2899
3306
  if (response.status === 401) throw new UnauthorizedStreamError();
2900
3307
  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}`);
3308
+ context.onOpen();
2901
3309
  const body = await response.json();
2902
3310
  const payload = typeof body === "object" && body !== null && "data" in body ? body.data : body;
2903
3311
  const items = pickArray(payload, "notifications");
@@ -3143,10 +3551,12 @@ var STREAM_PATH = "/api/notifications/stream";
3143
3551
  var SseTransport = class {
3144
3552
  name = "sse";
3145
3553
  #idleTimeout;
3554
+ #handshakeTimeout;
3146
3555
  /** Идентификатор последнего события — отправляется при переподключении. */
3147
3556
  #lastEventId;
3148
3557
  constructor(options = {}) {
3149
3558
  this.#idleTimeout = options.idleTimeout ?? 9e4;
3559
+ this.#handshakeTimeout = options.handshakeTimeout ?? 2e4;
3150
3560
  }
3151
3561
  async connect(context) {
3152
3562
  const token = await context.getToken();
@@ -3157,16 +3567,36 @@ var SseTransport = class {
3157
3567
  headers.set("Authorization", `Bearer ${token}`);
3158
3568
  headers.set("Cache-Control", "no-cache");
3159
3569
  if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
3160
- const response = await context.fetch(url, {
3161
- method: "GET",
3162
- headers,
3163
- signal: context.signal
3164
- });
3165
- if (response.status === 401) throw new UnauthorizedStreamError();
3166
- 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}`);
3167
- 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");
3168
- context.onOpen();
3169
- await this.#read(response.body, context);
3570
+ const controller = new AbortController();
3571
+ const relayAbort = () => controller.abort(context.signal.reason);
3572
+ if (context.signal.aborted) controller.abort(context.signal.reason);
3573
+ else context.signal.addEventListener("abort", relayAbort, { once: true });
3574
+ try {
3575
+ const response = await this.#handshake(url, headers, context, controller);
3576
+ if (response.status === 401) throw new UnauthorizedStreamError();
3577
+ 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}`);
3578
+ 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");
3579
+ context.onOpen();
3580
+ await this.#read(response.body, context);
3581
+ } finally {
3582
+ context.signal.removeEventListener("abort", relayAbort);
3583
+ }
3584
+ }
3585
+ /** Выполняет запрос потока, обрывая его, если ответ не пришёл за отведённое время. */
3586
+ async #handshake(url, headers, context, controller) {
3587
+ let expired = false;
3588
+ const timer = this.#handshakeTimeout > 0 ? setTimeout(() => {
3589
+ expired = true;
3590
+ 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"));
3591
+ }, this.#handshakeTimeout) : void 0;
3592
+ try {
3593
+ return await context.fetch(url, { method: "GET", headers, signal: controller.signal });
3594
+ } catch (error) {
3595
+ 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");
3596
+ throw error;
3597
+ } finally {
3598
+ if (timer !== void 0) clearTimeout(timer);
3599
+ }
3170
3600
  }
3171
3601
  async #read(body, context) {
3172
3602
  const reader = body.getReader();
@@ -3238,6 +3668,7 @@ function validateRealtimeOptions(options) {
3238
3668
  positiveInteger(options.maxAttempts, "maxAttempts");
3239
3669
  duration(options.pollInterval, "pollInterval", 1);
3240
3670
  duration(options.idleTimeout, "idleTimeout", 0);
3671
+ duration(options.handshakeTimeout, "handshakeTimeout", 0);
3241
3672
  if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) {
3242
3673
  throw new ItdConfigError(
3243
3674
  `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}`
@@ -3290,6 +3721,18 @@ var ItdRealtime = class {
3290
3721
  get transport() {
3291
3722
  return this.#transport.name;
3292
3723
  }
3724
+ /** Базовый URL клиента, создавшего поток. @internal */
3725
+ get baseUrl() {
3726
+ return this.#deps.baseUrl;
3727
+ }
3728
+ /** Идентификаторы аккаунта и сессии клиента, создавшего поток. @internal */
3729
+ getAuthIdentity() {
3730
+ return this.#deps.getAuthIdentity?.();
3731
+ }
3732
+ /** Непрозрачная область авторизации создавшего поток клиента. @internal */
3733
+ getAuthScope() {
3734
+ return this.#deps.getAuthScope?.();
3735
+ }
3293
3736
  /** Подписывается на событие потока. @returns функция отписки */
3294
3737
  on(event, listener) {
3295
3738
  return this.#emitter.on(event, listener);
@@ -3347,7 +3790,8 @@ var ItdRealtime = class {
3347
3790
  });
3348
3791
  }
3349
3792
  return new SseTransport({
3350
- ...this.#options.idleTimeout !== void 0 ? { idleTimeout: this.#options.idleTimeout } : {}
3793
+ ...this.#options.idleTimeout !== void 0 ? { idleTimeout: this.#options.idleTimeout } : {},
3794
+ ...this.#options.handshakeTimeout !== void 0 ? { handshakeTimeout: this.#options.handshakeTimeout } : {}
3351
3795
  });
3352
3796
  }
3353
3797
  /** Запускает попытку подключения; повторы планирует сам. */
@@ -3562,6 +4006,11 @@ var Paginator = class {
3562
4006
  #finished = false;
3563
4007
  #pagesLoaded = 0;
3564
4008
  constructor(options) {
4009
+ if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages < 1)) {
4010
+ throw new ItdConfigError(
4011
+ `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}`
4012
+ );
4013
+ }
3565
4014
  this.#options = options;
3566
4015
  this.#maxPages = options.maxPages ?? 1e3;
3567
4016
  this.#state = options.start ?? {};
@@ -3611,6 +4060,7 @@ var Paginator = class {
3611
4060
  * @param max сколько элементов достаточно; без него перебираются все страницы
3612
4061
  */
3613
4062
  async collect(max) {
4063
+ if (max !== void 0 && max <= 0) return [];
3614
4064
  const result = [];
3615
4065
  for await (const item of this) {
3616
4066
  result.push(item);
@@ -6086,6 +6536,13 @@ var VerificationResource = class extends BaseResource {
6086
6536
  };
6087
6537
 
6088
6538
  // src/client.ts
6539
+ var CLIENT_PLUGIN_REGISTRIES = /* @__PURE__ */ new WeakMap();
6540
+ function assertClientCanUsePlugin(client, plugin) {
6541
+ CLIENT_PLUGIN_REGISTRIES.get(client)?.assertCanAdd(plugin);
6542
+ }
6543
+ function assertClientCanUnusePlugin(client, name) {
6544
+ CLIENT_PLUGIN_REGISTRIES.get(client)?.assertCanRemove(name);
6545
+ }
6089
6546
  var ItdClient = class _ItdClient {
6090
6547
  #config;
6091
6548
  #http;
@@ -6123,13 +6580,10 @@ var ItdClient = class _ItdClient {
6123
6580
  subscription;
6124
6581
  /** Сведения о платформе: изменения, анонсы, баннер события. */
6125
6582
  platform;
6126
- /**
6127
- * Телеметрия просмотров.
6128
- *
6129
- * @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
6130
- */
6583
+ /** Телеметрия просмотров. */
6131
6584
  telemetry;
6132
6585
  constructor(options = {}, internals = {}) {
6586
+ CLIENT_PLUGIN_REGISTRIES.set(this, this.#plugins);
6133
6587
  const config = resolveConfig(options);
6134
6588
  this.#config = config;
6135
6589
  this.#jar = new CookieJar();
@@ -6141,18 +6595,22 @@ var ItdClient = class _ItdClient {
6141
6595
  this.#queues = queues;
6142
6596
  this.#ownsQueues = shared === void 0;
6143
6597
  let authManager;
6144
- const transport = new Transport(config, {
6145
- cookies: config.useCookieJar ? this.#jar : void 0,
6146
- getDeviceId: () => authManager.getDeviceId(),
6147
- onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
6148
- });
6598
+ const hooks = this.#plugins.hooks(config.hooks);
6599
+ const transport = new Transport(
6600
+ { ...config, hooks },
6601
+ {
6602
+ cookies: config.useCookieJar ? this.#jar : void 0,
6603
+ getDeviceId: () => authManager.getDeviceId(),
6604
+ onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
6605
+ }
6606
+ );
6149
6607
  this.#transport = transport;
6150
6608
  const pluginsLayer = createPluginsMiddleware(this.#plugins);
6151
6609
  const retriesLayer = createRetryMiddleware({
6152
6610
  retry: config.retry,
6153
6611
  rateLimitDelays: config.rateLimit?.retryDelays ?? [],
6154
6612
  pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
6155
- hooks: config.hooks,
6613
+ hooks,
6156
6614
  logger: config.logger,
6157
6615
  buildUrl: (request) => transport.buildUrl(request)
6158
6616
  });
@@ -6166,7 +6624,9 @@ var ItdClient = class _ItdClient {
6166
6624
  } : void 0;
6167
6625
  const authPipeline = composePipeline([pluginsLayer, retriesLayer], transport.send);
6168
6626
  const authHandler = (request) => authRetry && request.retry === void 0 ? authPipeline({ ...request, retry: authRetry }) : authPipeline(request);
6169
- authManager = new AuthManager(config, authHandler, this.#jar);
6627
+ authManager = new AuthManager(config, authHandler, this.#jar, {
6628
+ onAccountChange: () => this.#disconnectStreams()
6629
+ });
6170
6630
  this.#authManager = authManager;
6171
6631
  const middlewares = [];
6172
6632
  if (queues) {
@@ -6245,10 +6705,31 @@ var ItdClient = class _ItdClient {
6245
6705
  this.#plugins.add(plugin, {
6246
6706
  baseUrl: this.#config.baseUrl,
6247
6707
  logger: this.#config.logger,
6248
- getAuthScope: () => this.#authManager.getAuthScope()
6708
+ getAuthScope: () => this.#authManager.getAuthScope(),
6709
+ getAuthIdentity: () => this.#authManager.getAuthIdentity()
6249
6710
  });
6250
6711
  return this;
6251
6712
  }
6713
+ /** Имена подключённых плагинов в фактическом порядке выполнения обёрток. */
6714
+ pluginNames() {
6715
+ return this.#plugins.names();
6716
+ }
6717
+ /** Подключён ли плагин с таким именем. */
6718
+ hasPlugin(name) {
6719
+ return this.#plugins.has(name);
6720
+ }
6721
+ /**
6722
+ * Отключает плагин и освобождает заведённые им ресурсы.
6723
+ *
6724
+ * Новые запросы перестают видеть плагин сразу. Очистка дождётся логического запроса,
6725
+ * который уже проходил через его обёртку.
6726
+ *
6727
+ * @returns `false`, если такого плагина не было
6728
+ * @throws {ItdConfigError} если от плагина зависит другой подключённый плагин
6729
+ */
6730
+ unuse(name) {
6731
+ return this.#plugins.remove(name);
6732
+ }
6252
6733
  /**
6253
6734
  * Регистрирует сервис платформы — домен, отличный от основного.
6254
6735
  *
@@ -6305,7 +6786,8 @@ var ItdClient = class _ItdClient {
6305
6786
  * Создаёт поток уведомлений в реальном времени.
6306
6787
  *
6307
6788
  * Каждый вызов даёт новый независимый поток; обычно он нужен один на приложение.
6308
- * Соединение поднимается методом `connect()` и держится само.
6789
+ * Соединение поднимается методом `connect()` и держится само. Замена авторизации на токен
6790
+ * другого пользователя завершает все потоки клиента; смена только сессии их не затрагивает.
6309
6791
  *
6310
6792
  * @example
6311
6793
  * ```ts
@@ -6326,6 +6808,8 @@ var ItdClient = class _ItdClient {
6326
6808
  baseUrl: this.#config.baseUrl,
6327
6809
  fetch: this.#config.fetch,
6328
6810
  baseHeaders: (url) => this.#transport.platformHeaders(url),
6811
+ getAuthIdentity: () => this.#authManager.getCurrentAuthIdentity(),
6812
+ getAuthScope: () => this.#authManager.getAuthScope(),
6329
6813
  getToken: () => this.#authManager.getAccessToken(),
6330
6814
  refresh: () => this.#authManager.onUnauthorized(),
6331
6815
  fetchUnreadCount: () => this.notifications.count(),
@@ -6351,17 +6835,33 @@ var ItdClient = class _ItdClient {
6351
6835
  * ```ts
6352
6836
  * await using itd = new ItdClient({ auth: token });
6353
6837
  * // …работа…
6354
- * // close() вызовется сам на выходе из блока
6838
+ * // dispose() вызовется сам на выходе из блока
6355
6839
  * ```
6356
6840
  */
6357
6841
  async close() {
6358
- for (const stream of this.#streams) stream.disconnect();
6359
- this.#streams.clear();
6842
+ this.#disconnectStreams();
6360
6843
  if (this.#ownsQueues) this.#queues?.stop();
6361
6844
  }
6845
+ /**
6846
+ * Окончательно освобождает клиент: выполняет {@link close} и отключает все плагины.
6847
+ *
6848
+ * В отличие от `close()`, после `dispose()` плагины не восстанавливаются автоматически.
6849
+ * Сам клиент остаётся пригоден для обычных запросов; при необходимости плагины можно
6850
+ * подключить заново через {@link use}.
6851
+ */
6852
+ async dispose() {
6853
+ const results = await Promise.allSettled([this.close(), this.#plugins.dispose()]);
6854
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
6855
+ 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");
6856
+ }
6857
+ /** Завершает потоки до того, как запросы начнут использовать другой аккаунт. */
6858
+ #disconnectStreams() {
6859
+ for (const stream of [...this.#streams]) stream.disconnect();
6860
+ this.#streams.clear();
6861
+ }
6362
6862
  /** Позволяет использовать клиент с `await using`. */
6363
6863
  [Symbol.asyncDispose]() {
6364
- return this.close();
6864
+ return this.dispose();
6365
6865
  }
6366
6866
  static {
6367
6867
  if (typeof Symbol.asyncDispose !== "symbol") {
@@ -6560,10 +7060,13 @@ var ItdAccounts = class _ItdAccounts {
6560
7060
  #eventUnsubscribers = /* @__PURE__ */ new Map();
6561
7061
  /** Плагины для всех: и для уже заведённых аккаунтов, и для будущих. */
6562
7062
  #plugins;
7063
+ /** Имена плагинов, чья асинхронная очистка ещё не завершилась. */
7064
+ #removingPlugins = /* @__PURE__ */ new Set();
6563
7065
  /** Общая очередь. `undefined`, когда у каждого аккаунта своя. */
6564
7066
  #queues;
6565
7067
  #rateLimitScope;
6566
7068
  #emitter;
7069
+ #logger;
6567
7070
  #createClient;
6568
7071
  constructor(options = {}, internals = {}) {
6569
7072
  const { storage, plugins, rateLimitScope, ...base } = options;
@@ -6572,19 +7075,13 @@ var ItdAccounts = class _ItdAccounts {
6572
7075
  }
6573
7076
  this.#base = base;
6574
7077
  this.#storage = storage ?? new MemoryMultiTokenStorage();
6575
- this.#plugins = [];
6576
- for (const plugin of plugins ?? []) {
6577
- validatePluginDefinition(plugin);
6578
- if (this.#plugins.some((added) => added.name === plugin.name)) {
6579
- 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`);
6580
- }
6581
- this.#plugins.push(plugin);
6582
- }
7078
+ this.#plugins = orderPluginDefinitions(plugins ?? []);
6583
7079
  this.#rateLimitScope = rateLimitScope ?? "account";
6584
7080
  this.#createClient = internals.createClient ?? ((clientOptions, clientInternals) => new ItdClient(clientOptions, clientInternals));
6585
7081
  const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
6586
7082
  this.#queues = rateLimit ? new RequestQueuePool(rateLimit) : void 0;
6587
7083
  const logger = typeof base.logger === "object" ? base.logger : void 0;
7084
+ this.#logger = logger;
6588
7085
  this.#emitter = new Emitter(
6589
7086
  (error) => reportListenerError(logger, "\u0430\u043A\u043A\u0430\u0443\u043D\u0442\u043E\u0432", error)
6590
7087
  );
@@ -6650,6 +7147,11 @@ var ItdAccounts = class _ItdAccounts {
6650
7147
  for (const plugin of this.#plugins) client.use(plugin);
6651
7148
  } catch (error) {
6652
7149
  storageControl.revoke();
7150
+ if (client) {
7151
+ void client.dispose().catch((cleanupError) => {
7152
+ 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);
7153
+ });
7154
+ }
6653
7155
  throw error;
6654
7156
  }
6655
7157
  const unsubscribers = this.#forwardEvents(name, client);
@@ -6731,7 +7233,7 @@ var ItdAccounts = class _ItdAccounts {
6731
7233
  try {
6732
7234
  const errors = [];
6733
7235
  const closing = await Promise.allSettled([
6734
- client.close(),
7236
+ client.dispose(),
6735
7237
  storageControl?.drain() ?? Promise.resolve()
6736
7238
  ]);
6737
7239
  for (const result of closing) {
@@ -6759,14 +7261,64 @@ var ItdAccounts = class _ItdAccounts {
6759
7261
  * ```
6760
7262
  */
6761
7263
  use(plugin) {
6762
- validatePluginDefinition(plugin);
6763
- if (this.#plugins.some((added) => added.name === plugin?.name)) {
6764
- 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`);
7264
+ if (this.#removingPlugins.has(plugin?.name)) {
7265
+ throw new ItdConfigError(
7266
+ `\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()`
7267
+ );
6765
7268
  }
6766
- for (const client of this.#clients.values()) client.use(plugin);
6767
- this.#plugins.push(plugin);
7269
+ const ordered = orderPluginDefinitions([...this.#plugins, plugin]);
7270
+ const clients = [...this.#clients.values()];
7271
+ for (const client of clients) assertClientCanUsePlugin(client, plugin);
7272
+ const installed = [];
7273
+ try {
7274
+ for (const client of clients) {
7275
+ client.use(plugin);
7276
+ installed.push(client);
7277
+ }
7278
+ } catch (error) {
7279
+ for (const client of installed.reverse()) {
7280
+ void client.unuse(plugin.name).catch((cleanupError) => {
7281
+ this.#reportPluginCleanup(`\u043E\u0442\u043A\u0430\u0442\u0430 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \xAB${plugin.name}\xBB`, cleanupError);
7282
+ });
7283
+ }
7284
+ throw error;
7285
+ }
7286
+ this.#plugins.splice(0, this.#plugins.length, ...ordered);
6768
7287
  return this;
6769
7288
  }
7289
+ /** Имена общих плагинов в фактическом порядке выполнения обёрток. */
7290
+ pluginNames() {
7291
+ return this.#plugins.map((plugin) => plugin.name);
7292
+ }
7293
+ /** Подключён ли общий плагин с таким именем. */
7294
+ hasPlugin(name) {
7295
+ return this.#plugins.some((plugin) => plugin.name === name);
7296
+ }
7297
+ /**
7298
+ * Отключает общий плагин у существующих клиентов и не применяет его к будущим.
7299
+ *
7300
+ * @returns `false`, если такого плагина не было
7301
+ * @throws {ItdConfigError} если от плагина зависит другой общий плагин
7302
+ */
7303
+ async unuse(name) {
7304
+ const index = this.#plugins.findIndex((plugin) => plugin.name === name);
7305
+ if (index < 0) return false;
7306
+ assertPluginRemovable(this.#plugins, name);
7307
+ const clients = [...this.#clients.values()];
7308
+ for (const client of clients) assertClientCanUnusePlugin(client, name);
7309
+ this.#plugins.splice(index, 1);
7310
+ this.#removingPlugins.add(name);
7311
+ try {
7312
+ const results = await Promise.allSettled(clients.map((client) => client.unuse(name)));
7313
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
7314
+ if (errors.length > 0) {
7315
+ 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`);
7316
+ }
7317
+ return true;
7318
+ } finally {
7319
+ this.#removingPlugins.delete(name);
7320
+ }
7321
+ }
6770
7322
  /**
6771
7323
  * Подписывается на события авторизации всех аккаунтов сразу.
6772
7324
  *
@@ -6804,16 +7356,32 @@ var ItdAccounts = class _ItdAccounts {
6804
7356
  * ```ts
6805
7357
  * await using accounts = new ItdAccounts({ storage });
6806
7358
  * // …работа…
6807
- * // close() вызовется сам на выходе из блока
7359
+ * // dispose() вызовется сам на выходе из блока
6808
7360
  * ```
6809
7361
  */
6810
7362
  async close() {
6811
7363
  await Promise.all([...this.#clients.values()].map((client) => client.close()));
6812
7364
  this.#queues?.stop();
6813
7365
  }
7366
+ /**
7367
+ * Окончательно освобождает контейнер и отключает общие плагины у всех аккаунтов.
7368
+ *
7369
+ * Для временной остановки потоков и очереди без отключения плагинов используйте
7370
+ * {@link close}.
7371
+ */
7372
+ async dispose() {
7373
+ const results = await Promise.allSettled([
7374
+ ...[...this.#clients.values()].map((client) => client.dispose()),
7375
+ Promise.resolve().then(() => this.#queues?.stop())
7376
+ ]);
7377
+ this.#plugins.splice(0);
7378
+ this.#removingPlugins.clear();
7379
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
7380
+ 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");
7381
+ }
6814
7382
  /** Позволяет использовать контейнер с `await using`. */
6815
7383
  [Symbol.asyncDispose]() {
6816
- return this.close();
7384
+ return this.dispose();
6817
7385
  }
6818
7386
  static {
6819
7387
  if (typeof Symbol.asyncDispose !== "symbol") {
@@ -6839,6 +7407,12 @@ var ItdAccounts = class _ItdAccounts {
6839
7407
  storage
6840
7408
  };
6841
7409
  }
7410
+ /** Не теряет ошибку фонового teardown, который синхронный API не может await-нуть. */
7411
+ #reportPluginCleanup(scope, error) {
7412
+ 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}`;
7413
+ if (this.#logger) this.#logger.error(message, error);
7414
+ else console.error(`[itd-api] ${message}`, error);
7415
+ }
6842
7416
  /** Ретранслирует события клиента наружу, добавляя к ним имя аккаунта. */
6843
7417
  #forwardEvents(account, client) {
6844
7418
  return [
@@ -7185,5 +7759,5 @@ function statusDays(service) {
7185
7759
  }
7186
7760
 
7187
7761
  export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, BUILT_IN_SERVICES, CommentSort, DEFAULT_BASE_URL, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryMultiTokenStorage, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STATUS_SERVICE, STREAM_PATH, ServiceRegistry, ServiceState, SignInStatus, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, autoSpans, canonicalNotificationType, comment, copySession, createAccounts, createClient, createMultiTokenStorage, createRecordMultiStorage, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, renderSpans, report, resolveNotificationUrl, scopedTokenStorage, statusDays, toDate, utcStampToIso };
7188
- //# sourceMappingURL=chunk-SMV7TF5P.js.map
7189
- //# sourceMappingURL=chunk-SMV7TF5P.js.map
7762
+ //# sourceMappingURL=chunk-6FB4HTKH.js.map
7763
+ //# sourceMappingURL=chunk-6FB4HTKH.js.map