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
@@ -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
  };
@@ -674,18 +674,24 @@ function decodeBase64Url(segment) {
674
674
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
675
675
  return new TextDecoder().decode(bytes);
676
676
  }
677
- function readTokenSubject(token) {
677
+ function readTokenIdentity(token) {
678
678
  try {
679
679
  const payload = token.split(".")[1];
680
- if (!payload) return void 0;
680
+ if (!payload) return {};
681
681
  const parsed = JSON.parse(decodeBase64Url(payload));
682
- if (typeof parsed !== "object" || parsed === null) return void 0;
683
- const subject = parsed.sub;
684
- return typeof subject === "string" && subject.length > 0 ? subject : void 0;
682
+ if (typeof parsed !== "object" || parsed === null) return {};
683
+ const { sub, sid } = parsed;
684
+ return {
685
+ ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
686
+ ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {}
687
+ };
685
688
  } catch {
686
- return void 0;
689
+ return {};
687
690
  }
688
691
  }
692
+ function readTokenSubject(token) {
693
+ return readTokenIdentity(token).subject;
694
+ }
689
695
 
690
696
  // src/core/runtime.ts
691
697
  var RuntimeMode = Object.freeze({
@@ -878,6 +884,7 @@ var AuthManager = class {
878
884
  #send;
879
885
  #jar;
880
886
  #emitter;
887
+ #hooks;
881
888
  /** `undefined` — сессия ещё не читалась из хранилища. */
882
889
  #session;
883
890
  /**
@@ -889,8 +896,12 @@ var AuthManager = class {
889
896
  #refreshing = null;
890
897
  /** Общий промис входа по логину и паролю. */
891
898
  #signingIn = null;
892
- /** Непрозрачная версия владельца клиента для разделения состояния плагинов. */
899
+ /** Fallback-область для изоляции состояния плагинов, когда JWT-идентичность недоступна. */
893
900
  #authScope = nextAuthScope();
901
+ /** Последний токен внешнего источника; `undefined` означает, что источник ещё не читался. */
902
+ #externalToken;
903
+ /** Идентификаторы последнего токена внешнего источника для синхронных потребителей. */
904
+ #externalIdentity;
894
905
  /**
895
906
  * Идентификатор устройства.
896
907
  *
@@ -900,10 +911,20 @@ var AuthManager = class {
900
911
  #deviceId;
901
912
  /** Общий промис первичной выдачи `deviceId` — чтобы параллельные запросы получили один. */
902
913
  #deviceIdLoading = null;
903
- constructor(config, send, jar) {
914
+ /**
915
+ * Счётчик смен владельца авторизации.
916
+ *
917
+ * Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
918
+ * `setSession`, `setAccessToken`, вход. `#performRefresh` снимает его значение перед
919
+ * сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
920
+ * воскресить уже очищенную сессию поверх `signOut`.
921
+ */
922
+ #authEpoch = 0;
923
+ constructor(config, send, jar, hooks = {}) {
904
924
  this.#config = config;
905
925
  this.#send = send;
906
926
  this.#jar = jar;
927
+ this.#hooks = hooks;
907
928
  this.#emitter = new Emitter(
908
929
  (error) => reportListenerError(config.logger, "\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", error)
909
930
  );
@@ -916,13 +937,67 @@ var AuthManager = class {
916
937
  get once() {
917
938
  return this.#emitter.once.bind(this.#emitter);
918
939
  }
919
- /** Непрозрачная область авторизации; токен и идентификатор пользователя не раскрываются. */
940
+ /**
941
+ * Непрозрачная fallback-область авторизации.
942
+ *
943
+ * Изолирует состояние плагинов, когда идентичность аккаунта из JWT недоступна (непрозрачный
944
+ * токен): значение уникально для владельца и сменяется при замене или очистке сессии.
945
+ */
920
946
  getAuthScope() {
921
947
  return this.#authScope;
922
948
  }
949
+ /** Загружает сессию и возвращает идентификаторы аккаунта и сессии для плагинов. */
950
+ async getAuthIdentity() {
951
+ const session = await this.#loadSession();
952
+ if (session?.accessToken) return this.#identityForToken(session.accessToken);
953
+ const auth = this.#config.auth;
954
+ if (typeof auth === "object" && auth !== null && "getToken" in auth) {
955
+ const token = await this.#readExternalToken(() => auth.getToken());
956
+ return this.#identityForToken(token ?? void 0);
957
+ }
958
+ return {};
959
+ }
960
+ /** Идентификаторы аккаунта и сессии без чтения хранилища. @internal */
961
+ getCurrentAuthIdentity() {
962
+ const session = this.#session === void 0 ? this.#sessionFromConfig(this.#config.auth) : this.#session;
963
+ return session?.accessToken ? this.#identityForToken(session.accessToken) : this.#externalIdentity ?? {};
964
+ }
923
965
  #rotateAuthScope() {
924
966
  this.#authScope = nextAuthScope();
925
967
  }
968
+ /** Отмечает смену владельца авторизации — обесценивает результат идущего обновления. */
969
+ #invalidateInFlight() {
970
+ this.#authEpoch += 1;
971
+ }
972
+ #identityForToken(accessToken) {
973
+ const token = accessToken ? readTokenIdentity(accessToken) : {};
974
+ return {
975
+ ...token.subject ? { userId: token.subject } : {},
976
+ ...token.sessionId ? { sessionId: token.sessionId } : {}
977
+ };
978
+ }
979
+ async #readExternalToken(getToken) {
980
+ const token = await getToken() ?? null;
981
+ const current = this.#identityForToken(token ?? void 0);
982
+ if (this.#externalToken !== void 0 && this.#externalToken !== token) {
983
+ this.#rotateAuthScope();
984
+ const previous = this.#externalIdentity ?? {};
985
+ const changed = previous.userId !== void 0 && current.userId !== void 0 ? previous.userId !== current.userId : true;
986
+ if (changed) this.#hooks.onAccountChange?.();
987
+ }
988
+ this.#externalToken = token;
989
+ this.#externalIdentity = current;
990
+ return token;
991
+ }
992
+ /** Меняет fallback и завершает realtime, только если фактически сменился аккаунт. */
993
+ #transitionAuth(accessToken, rotateFallback = true) {
994
+ const knownPrevious = this.#session !== void 0;
995
+ const previous = this.#identityForToken(this.#session?.accessToken);
996
+ const current = this.#identityForToken(accessToken);
997
+ const changed = previous.userId !== void 0 && current.userId !== void 0 ? previous.userId !== current.userId : previous.userId !== current.userId || rotateFallback;
998
+ if (rotateFallback || changed) this.#rotateAuthScope();
999
+ if (knownPrevious && changed) this.#hooks.onAccountChange?.();
1000
+ }
926
1001
  /**
927
1002
  * Есть ли признак живой refresh-сессии.
928
1003
  *
@@ -983,7 +1058,7 @@ var AuthManager = class {
983
1058
  const auth = this.#config.auth;
984
1059
  if (!auth) return null;
985
1060
  if (typeof auth === "object" && "getToken" in auth) {
986
- return await auth.getToken() ?? null;
1061
+ return this.#readExternalToken(() => auth.getToken());
987
1062
  }
988
1063
  if (typeof auth === "object" && "email" in auth) {
989
1064
  return this.#signInWithCredentials(auth);
@@ -1038,7 +1113,9 @@ var AuthManager = class {
1038
1113
  }
1039
1114
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
1040
1115
  async setAccessToken(accessToken) {
1041
- this.#rotateAuthScope();
1116
+ await this.#loadSession();
1117
+ this.#invalidateInFlight();
1118
+ this.#transitionAuth(accessToken);
1042
1119
  await this.#saveSession({ ...this.#session ?? {}, accessToken, obtainedAt: Date.now() });
1043
1120
  this.#emitter.emit("tokens", { accessToken });
1044
1121
  }
@@ -1059,7 +1136,9 @@ var AuthManager = class {
1059
1136
  }
1060
1137
  /** Заменяет сессию и связанные с ней cookie целиком. */
1061
1138
  async setSession(session) {
1062
- this.#rotateAuthScope();
1139
+ await this.#loadSession();
1140
+ this.#invalidateInFlight();
1141
+ this.#transitionAuth(session.accessToken);
1063
1142
  this.#jar.clear();
1064
1143
  this.#jar.deserialize(session.cookies);
1065
1144
  if (session.deviceId) this.#deviceId = session.deviceId;
@@ -1074,8 +1153,10 @@ var AuthManager = class {
1074
1153
  * новую запись в списке сессий.
1075
1154
  */
1076
1155
  async clear() {
1156
+ await this.#loadSession();
1157
+ this.#invalidateInFlight();
1158
+ this.#transitionAuth(void 0);
1077
1159
  this.#session = null;
1078
- this.#rotateAuthScope();
1079
1160
  this.#jar.clear();
1080
1161
  await this.#config.storage.clear();
1081
1162
  if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
@@ -1156,6 +1237,7 @@ var AuthManager = class {
1156
1237
  if (!this.#hasRefreshSession()) {
1157
1238
  return this.#reloginOrNull();
1158
1239
  }
1240
+ const epoch = this.#authEpoch;
1159
1241
  try {
1160
1242
  const payload = await this.#send({
1161
1243
  method: "POST",
@@ -1168,10 +1250,14 @@ var AuthManager = class {
1168
1250
  });
1169
1251
  const accessToken = readAccessToken(payload);
1170
1252
  if (!accessToken) return this.#reloginOrNull();
1253
+ if (this.#authEpoch !== epoch) {
1254
+ return this.#session?.accessToken ?? null;
1255
+ }
1171
1256
  const rotated = this.#jar.getValue(
1172
1257
  REFRESH_COOKIE,
1173
1258
  this.#config.baseUrl + REFRESH_COOKIE_PATH
1174
1259
  );
1260
+ this.#transitionAuth(accessToken, false);
1175
1261
  await this.#saveSession({
1176
1262
  ...this.#session ?? {},
1177
1263
  accessToken,
@@ -1182,8 +1268,11 @@ var AuthManager = class {
1182
1268
  return accessToken;
1183
1269
  } catch (error) {
1184
1270
  if (error instanceof ItdApiError) {
1271
+ if (this.#authEpoch !== epoch) {
1272
+ return this.#session?.accessToken ?? null;
1273
+ }
1274
+ this.#transitionAuth(void 0);
1185
1275
  this.#session = null;
1186
- this.#rotateAuthScope();
1187
1276
  this.#jar.clear();
1188
1277
  await this.#config.storage.clear();
1189
1278
  const relogged = await this.#reloginOrNull();
@@ -1251,7 +1340,8 @@ var AuthManager = class {
1251
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."
1252
1341
  );
1253
1342
  }
1254
- this.#rotateAuthScope();
1343
+ this.#invalidateInFlight();
1344
+ this.#transitionAuth(accessToken);
1255
1345
  await this.#saveSession({ accessToken, obtainedAt: Date.now() });
1256
1346
  this.#emitter.emit("tokens", { accessToken });
1257
1347
  this.#emitter.emit("signIn", { accessToken });
@@ -1318,7 +1408,7 @@ function normalizeBaseUrl(baseUrl) {
1318
1408
  }
1319
1409
 
1320
1410
  // src/core/version.ts
1321
- var LIBRARY_VERSION = "0.0.10";
1411
+ var LIBRARY_VERSION = "0.1.0";
1322
1412
 
1323
1413
  // src/core/config.ts
1324
1414
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
@@ -1603,6 +1693,396 @@ function withLayerHeaders(request, headers) {
1603
1693
  return { ...request, layerHeaders: { ...request.layerHeaders, ...headers } };
1604
1694
  }
1605
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
+
1606
2086
  // src/core/retry.ts
1607
2087
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
1608
2088
  function isRetryable(error, method, retryWrites) {
@@ -1658,10 +2138,7 @@ function createQueueMiddleware(schedule) {
1658
2138
  return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
1659
2139
  }
1660
2140
  function createPluginsMiddleware(plugins) {
1661
- return (request, next) => {
1662
- if (plugins.size === 0) return next(request);
1663
- return plugins.run(request, next);
1664
- };
2141
+ return (request, next) => plugins.run(request, next);
1665
2142
  }
1666
2143
  function createServicesMiddleware(registry) {
1667
2144
  return async (request, next) => {
@@ -1733,16 +2210,21 @@ function createRetryMiddleware(deps) {
1733
2210
  } catch (error) {
1734
2211
  const delay = nextDelay(error, attempt, request, method, backoff);
1735
2212
  if (delay === void 0) throw error;
1736
- await deps.hooks.onRetry?.({
1737
- method,
1738
- path: request.path,
1739
- url: deps.buildUrl(request),
1740
- // Умолчания транспорта добавляются после слоя повторов и сюда не входят.
1741
- headers: new Headers({ ...request.layerHeaders, ...request.headers }),
1742
- attempt,
1743
- error,
1744
- delay
1745
- });
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
+ );
1746
2228
  deps.logger?.debug(
1747
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`
1748
2230
  );
@@ -1752,104 +2234,6 @@ function createRetryMiddleware(deps) {
1752
2234
  };
1753
2235
  }
1754
2236
 
1755
- // src/core/plugins.ts
1756
- var NO_KEYS = /* @__PURE__ */ new Set();
1757
- var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
1758
- "signal",
1759
- "timeout",
1760
- "headers",
1761
- "retry",
1762
- "method",
1763
- "path",
1764
- "service",
1765
- "baseUrl",
1766
- "query",
1767
- "body",
1768
- "skipAuth",
1769
- "skipAuthRefresh",
1770
- "skipQueue",
1771
- "raw"
1772
- ]);
1773
- function validatePluginDefinition(plugin) {
1774
- if (typeof plugin?.install !== "function") {
1775
- 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()");
1776
- }
1777
- const name = plugin.name;
1778
- if (typeof name !== "string" || name.trim() === "") {
1779
- 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");
1780
- }
1781
- const keys = plugin.optionKeys ?? [];
1782
- for (const key of keys) {
1783
- if (typeof key !== "string" || key.trim() === "") {
1784
- 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`);
1785
- }
1786
- if (RESERVED_OPTION_KEYS.has(key)) {
1787
- throw new ItdConfigError(
1788
- `\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(", ")}`
1789
- );
1790
- }
1791
- }
1792
- }
1793
- var PluginRegistry = class {
1794
- #transformers = [];
1795
- #optionKeys = /* @__PURE__ */ new Set();
1796
- #names = /* @__PURE__ */ new Set();
1797
- /** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
1798
- get size() {
1799
- return this.#transformers.length;
1800
- }
1801
- /** Имена опций запроса, заявленные плагинами. */
1802
- get optionKeys() {
1803
- return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
1804
- }
1805
- /**
1806
- * Подключает плагин.
1807
- *
1808
- * @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
1809
- * имя опции
1810
- */
1811
- add(plugin, context) {
1812
- validatePluginDefinition(plugin);
1813
- const name = plugin.name;
1814
- if (this.#names.has(name)) {
1815
- throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
1816
- }
1817
- const keys = plugin.optionKeys ?? [];
1818
- const before = this.#transformers.length;
1819
- try {
1820
- plugin.install({
1821
- ...context,
1822
- use: (transformer) => {
1823
- if (typeof transformer !== "function") {
1824
- 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`);
1825
- }
1826
- this.#transformers.push(transformer);
1827
- }
1828
- });
1829
- } catch (error) {
1830
- this.#transformers.length = before;
1831
- throw error;
1832
- }
1833
- this.#names.add(name);
1834
- for (const key of keys) this.#optionKeys.add(key);
1835
- }
1836
- /**
1837
- * Прогоняет запрос через цепочку обёрток.
1838
- *
1839
- * Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
1840
- * а обёрток единицы — экономить тут не на чем.
1841
- *
1842
- * @param execute настоящий запрос, вызывается самой внутренней обёрткой
1843
- */
1844
- run(request, execute) {
1845
- const chain = this.#transformers.reduceRight(
1846
- (next, transformer) => (current) => transformer(current, next),
1847
- execute
1848
- );
1849
- return chain(request);
1850
- }
1851
- };
1852
-
1853
2237
  // src/core/rate-limit.ts
1854
2238
  function queueAbortError() {
1855
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");
@@ -2447,7 +2831,7 @@ var Transport = class {
2447
2831
  }
2448
2832
  }
2449
2833
  const context = { method, path: request.path, url, headers, attempt };
2450
- await this.#config.hooks.onRequest?.(context);
2834
+ await dispatchRequestHook(this.#config.hooks, "onRequest", context, request);
2451
2835
  const timeout = request.timeout ?? this.#config.timeout;
2452
2836
  const abort = createAbortBundle(request.signal, timeout);
2453
2837
  const startedAt = Date.now();
@@ -2468,7 +2852,12 @@ var Transport = class {
2468
2852
  } catch (error) {
2469
2853
  const duration2 = Date.now() - startedAt;
2470
2854
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2471
- 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
+ );
2472
2861
  this.#config.logger?.warn(
2473
2862
  `\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`
2474
2863
  );
@@ -2480,12 +2869,17 @@ var Transport = class {
2480
2869
  }
2481
2870
  if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2482
2871
  if (response.ok) {
2483
- await this.#config.hooks.onResponse?.({
2484
- ...context,
2485
- status: response.status,
2486
- duration: Date.now() - startedAt,
2487
- response
2488
- });
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
+ );
2489
2883
  }
2490
2884
  const payload = await this.#readBodyOrFail(
2491
2885
  response,
@@ -2506,7 +2900,12 @@ var Transport = class {
2506
2900
  response,
2507
2901
  body: payload
2508
2902
  });
2509
- await this.#config.hooks.onError?.({ ...context, duration, error });
2903
+ await dispatchRequestHook(
2904
+ this.#config.hooks,
2905
+ "onError",
2906
+ { ...context, duration, error },
2907
+ request
2908
+ );
2510
2909
  this.#config.logger?.warn(
2511
2910
  `\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
2512
2911
  );
@@ -2527,11 +2926,16 @@ var Transport = class {
2527
2926
  await response.body?.cancel().catch(() => {
2528
2927
  });
2529
2928
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2530
- await this.#config.hooks.onError?.({
2531
- ...context,
2532
- duration: Date.now() - startedAt,
2533
- error: failure
2534
- });
2929
+ await dispatchRequestHook(
2930
+ this.#config.hooks,
2931
+ "onError",
2932
+ {
2933
+ ...context,
2934
+ duration: Date.now() - startedAt,
2935
+ error: failure
2936
+ },
2937
+ request
2938
+ );
2535
2939
  this.#config.logger?.warn(
2536
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}`
2537
2941
  );
@@ -2582,12 +2986,16 @@ var Transport = class {
2582
2986
  }
2583
2987
  /** Превращает исключение `fetch` в понятную ошибку библиотеки. */
2584
2988
  #toTransportError(error, abort, request, method, timeout) {
2585
- const aborted = error instanceof Error && error.name === "AbortError";
2989
+ const aborted = abort.signal.aborted || error instanceof Error && error.name === "AbortError";
2586
2990
  if (aborted && abort.timedOut()) {
2587
2991
  return new ItdTimeoutError({ timeout, method, path: request.path });
2588
2992
  }
2589
2993
  if (aborted) {
2590
- 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
+ );
2591
2999
  }
2592
3000
  return new ItdNetworkError(
2593
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)}`,
@@ -2885,7 +3293,6 @@ var PollTransport = class {
2885
3293
  const seen = /* @__PURE__ */ new Set();
2886
3294
  let firstRun = true;
2887
3295
  let lastUnreadCount;
2888
- context.onOpen();
2889
3296
  while (!context.signal.aborted) {
2890
3297
  const token = await context.getToken();
2891
3298
  if (!token) throw new UnauthorizedStreamError();
@@ -2900,6 +3307,7 @@ var PollTransport = class {
2900
3307
  });
2901
3308
  if (response.status === 401) throw new UnauthorizedStreamError();
2902
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();
2903
3311
  const body = await response.json();
2904
3312
  const payload = typeof body === "object" && body !== null && "data" in body ? body.data : body;
2905
3313
  const items = pickArray(payload, "notifications");
@@ -3145,10 +3553,12 @@ var STREAM_PATH = "/api/notifications/stream";
3145
3553
  var SseTransport = class {
3146
3554
  name = "sse";
3147
3555
  #idleTimeout;
3556
+ #handshakeTimeout;
3148
3557
  /** Идентификатор последнего события — отправляется при переподключении. */
3149
3558
  #lastEventId;
3150
3559
  constructor(options = {}) {
3151
3560
  this.#idleTimeout = options.idleTimeout ?? 9e4;
3561
+ this.#handshakeTimeout = options.handshakeTimeout ?? 2e4;
3152
3562
  }
3153
3563
  async connect(context) {
3154
3564
  const token = await context.getToken();
@@ -3159,16 +3569,36 @@ var SseTransport = class {
3159
3569
  headers.set("Authorization", `Bearer ${token}`);
3160
3570
  headers.set("Cache-Control", "no-cache");
3161
3571
  if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
3162
- const response = await context.fetch(url, {
3163
- method: "GET",
3164
- headers,
3165
- signal: context.signal
3166
- });
3167
- if (response.status === 401) throw new UnauthorizedStreamError();
3168
- 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}`);
3169
- 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");
3170
- context.onOpen();
3171
- 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
+ }
3172
3602
  }
3173
3603
  async #read(body, context) {
3174
3604
  const reader = body.getReader();
@@ -3240,6 +3670,7 @@ function validateRealtimeOptions(options) {
3240
3670
  positiveInteger(options.maxAttempts, "maxAttempts");
3241
3671
  duration(options.pollInterval, "pollInterval", 1);
3242
3672
  duration(options.idleTimeout, "idleTimeout", 0);
3673
+ duration(options.handshakeTimeout, "handshakeTimeout", 0);
3243
3674
  if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) {
3244
3675
  throw new ItdConfigError(
3245
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}`
@@ -3292,6 +3723,18 @@ var ItdRealtime = class {
3292
3723
  get transport() {
3293
3724
  return this.#transport.name;
3294
3725
  }
3726
+ /** Базовый URL клиента, создавшего поток. @internal */
3727
+ get baseUrl() {
3728
+ return this.#deps.baseUrl;
3729
+ }
3730
+ /** Идентификаторы аккаунта и сессии клиента, создавшего поток. @internal */
3731
+ getAuthIdentity() {
3732
+ return this.#deps.getAuthIdentity?.();
3733
+ }
3734
+ /** Непрозрачная область авторизации создавшего поток клиента. @internal */
3735
+ getAuthScope() {
3736
+ return this.#deps.getAuthScope?.();
3737
+ }
3295
3738
  /** Подписывается на событие потока. @returns функция отписки */
3296
3739
  on(event, listener) {
3297
3740
  return this.#emitter.on(event, listener);
@@ -3349,7 +3792,8 @@ var ItdRealtime = class {
3349
3792
  });
3350
3793
  }
3351
3794
  return new SseTransport({
3352
- ...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 } : {}
3353
3797
  });
3354
3798
  }
3355
3799
  /** Запускает попытку подключения; повторы планирует сам. */
@@ -3564,6 +4008,11 @@ var Paginator = class {
3564
4008
  #finished = false;
3565
4009
  #pagesLoaded = 0;
3566
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
+ }
3567
4016
  this.#options = options;
3568
4017
  this.#maxPages = options.maxPages ?? 1e3;
3569
4018
  this.#state = options.start ?? {};
@@ -3613,6 +4062,7 @@ var Paginator = class {
3613
4062
  * @param max сколько элементов достаточно; без него перебираются все страницы
3614
4063
  */
3615
4064
  async collect(max) {
4065
+ if (max !== void 0 && max <= 0) return [];
3616
4066
  const result = [];
3617
4067
  for await (const item of this) {
3618
4068
  result.push(item);
@@ -6088,6 +6538,13 @@ var VerificationResource = class extends BaseResource {
6088
6538
  };
6089
6539
 
6090
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
+ }
6091
6548
  var ItdClient = class _ItdClient {
6092
6549
  #config;
6093
6550
  #http;
@@ -6125,13 +6582,10 @@ var ItdClient = class _ItdClient {
6125
6582
  subscription;
6126
6583
  /** Сведения о платформе: изменения, анонсы, баннер события. */
6127
6584
  platform;
6128
- /**
6129
- * Телеметрия просмотров.
6130
- *
6131
- * @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
6132
- */
6585
+ /** Телеметрия просмотров. */
6133
6586
  telemetry;
6134
6587
  constructor(options = {}, internals = {}) {
6588
+ CLIENT_PLUGIN_REGISTRIES.set(this, this.#plugins);
6135
6589
  const config = resolveConfig(options);
6136
6590
  this.#config = config;
6137
6591
  this.#jar = new CookieJar();
@@ -6143,18 +6597,22 @@ var ItdClient = class _ItdClient {
6143
6597
  this.#queues = queues;
6144
6598
  this.#ownsQueues = shared === void 0;
6145
6599
  let authManager;
6146
- const transport = new Transport(config, {
6147
- cookies: config.useCookieJar ? this.#jar : void 0,
6148
- getDeviceId: () => authManager.getDeviceId(),
6149
- onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
6150
- });
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
+ );
6151
6609
  this.#transport = transport;
6152
6610
  const pluginsLayer = createPluginsMiddleware(this.#plugins);
6153
6611
  const retriesLayer = createRetryMiddleware({
6154
6612
  retry: config.retry,
6155
6613
  rateLimitDelays: config.rateLimit?.retryDelays ?? [],
6156
6614
  pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
6157
- hooks: config.hooks,
6615
+ hooks,
6158
6616
  logger: config.logger,
6159
6617
  buildUrl: (request) => transport.buildUrl(request)
6160
6618
  });
@@ -6168,7 +6626,9 @@ var ItdClient = class _ItdClient {
6168
6626
  } : void 0;
6169
6627
  const authPipeline = composePipeline([pluginsLayer, retriesLayer], transport.send);
6170
6628
  const authHandler = (request) => authRetry && request.retry === void 0 ? authPipeline({ ...request, retry: authRetry }) : authPipeline(request);
6171
- authManager = new AuthManager(config, authHandler, this.#jar);
6629
+ authManager = new AuthManager(config, authHandler, this.#jar, {
6630
+ onAccountChange: () => this.#disconnectStreams()
6631
+ });
6172
6632
  this.#authManager = authManager;
6173
6633
  const middlewares = [];
6174
6634
  if (queues) {
@@ -6247,10 +6707,31 @@ var ItdClient = class _ItdClient {
6247
6707
  this.#plugins.add(plugin, {
6248
6708
  baseUrl: this.#config.baseUrl,
6249
6709
  logger: this.#config.logger,
6250
- getAuthScope: () => this.#authManager.getAuthScope()
6710
+ getAuthScope: () => this.#authManager.getAuthScope(),
6711
+ getAuthIdentity: () => this.#authManager.getAuthIdentity()
6251
6712
  });
6252
6713
  return this;
6253
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
+ }
6254
6735
  /**
6255
6736
  * Регистрирует сервис платформы — домен, отличный от основного.
6256
6737
  *
@@ -6307,7 +6788,8 @@ var ItdClient = class _ItdClient {
6307
6788
  * Создаёт поток уведомлений в реальном времени.
6308
6789
  *
6309
6790
  * Каждый вызов даёт новый независимый поток; обычно он нужен один на приложение.
6310
- * Соединение поднимается методом `connect()` и держится само.
6791
+ * Соединение поднимается методом `connect()` и держится само. Замена авторизации на токен
6792
+ * другого пользователя завершает все потоки клиента; смена только сессии их не затрагивает.
6311
6793
  *
6312
6794
  * @example
6313
6795
  * ```ts
@@ -6328,6 +6810,8 @@ var ItdClient = class _ItdClient {
6328
6810
  baseUrl: this.#config.baseUrl,
6329
6811
  fetch: this.#config.fetch,
6330
6812
  baseHeaders: (url) => this.#transport.platformHeaders(url),
6813
+ getAuthIdentity: () => this.#authManager.getCurrentAuthIdentity(),
6814
+ getAuthScope: () => this.#authManager.getAuthScope(),
6331
6815
  getToken: () => this.#authManager.getAccessToken(),
6332
6816
  refresh: () => this.#authManager.onUnauthorized(),
6333
6817
  fetchUnreadCount: () => this.notifications.count(),
@@ -6353,17 +6837,33 @@ var ItdClient = class _ItdClient {
6353
6837
  * ```ts
6354
6838
  * await using itd = new ItdClient({ auth: token });
6355
6839
  * // …работа…
6356
- * // close() вызовется сам на выходе из блока
6840
+ * // dispose() вызовется сам на выходе из блока
6357
6841
  * ```
6358
6842
  */
6359
6843
  async close() {
6360
- for (const stream of this.#streams) stream.disconnect();
6361
- this.#streams.clear();
6844
+ this.#disconnectStreams();
6362
6845
  if (this.#ownsQueues) this.#queues?.stop();
6363
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
+ }
6859
+ /** Завершает потоки до того, как запросы начнут использовать другой аккаунт. */
6860
+ #disconnectStreams() {
6861
+ for (const stream of [...this.#streams]) stream.disconnect();
6862
+ this.#streams.clear();
6863
+ }
6364
6864
  /** Позволяет использовать клиент с `await using`. */
6365
6865
  [Symbol.asyncDispose]() {
6366
- return this.close();
6866
+ return this.dispose();
6367
6867
  }
6368
6868
  static {
6369
6869
  if (typeof Symbol.asyncDispose !== "symbol") {
@@ -6562,10 +7062,13 @@ var ItdAccounts = class _ItdAccounts {
6562
7062
  #eventUnsubscribers = /* @__PURE__ */ new Map();
6563
7063
  /** Плагины для всех: и для уже заведённых аккаунтов, и для будущих. */
6564
7064
  #plugins;
7065
+ /** Имена плагинов, чья асинхронная очистка ещё не завершилась. */
7066
+ #removingPlugins = /* @__PURE__ */ new Set();
6565
7067
  /** Общая очередь. `undefined`, когда у каждого аккаунта своя. */
6566
7068
  #queues;
6567
7069
  #rateLimitScope;
6568
7070
  #emitter;
7071
+ #logger;
6569
7072
  #createClient;
6570
7073
  constructor(options = {}, internals = {}) {
6571
7074
  const { storage, plugins, rateLimitScope, ...base } = options;
@@ -6574,19 +7077,13 @@ var ItdAccounts = class _ItdAccounts {
6574
7077
  }
6575
7078
  this.#base = base;
6576
7079
  this.#storage = storage ?? new MemoryMultiTokenStorage();
6577
- this.#plugins = [];
6578
- for (const plugin of plugins ?? []) {
6579
- validatePluginDefinition(plugin);
6580
- if (this.#plugins.some((added) => added.name === plugin.name)) {
6581
- 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`);
6582
- }
6583
- this.#plugins.push(plugin);
6584
- }
7080
+ this.#plugins = orderPluginDefinitions(plugins ?? []);
6585
7081
  this.#rateLimitScope = rateLimitScope ?? "account";
6586
7082
  this.#createClient = internals.createClient ?? ((clientOptions, clientInternals) => new ItdClient(clientOptions, clientInternals));
6587
7083
  const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
6588
7084
  this.#queues = rateLimit ? new RequestQueuePool(rateLimit) : void 0;
6589
7085
  const logger = typeof base.logger === "object" ? base.logger : void 0;
7086
+ this.#logger = logger;
6590
7087
  this.#emitter = new Emitter(
6591
7088
  (error) => reportListenerError(logger, "\u0430\u043A\u043A\u0430\u0443\u043D\u0442\u043E\u0432", error)
6592
7089
  );
@@ -6652,6 +7149,11 @@ var ItdAccounts = class _ItdAccounts {
6652
7149
  for (const plugin of this.#plugins) client.use(plugin);
6653
7150
  } catch (error) {
6654
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
+ }
6655
7157
  throw error;
6656
7158
  }
6657
7159
  const unsubscribers = this.#forwardEvents(name, client);
@@ -6733,7 +7235,7 @@ var ItdAccounts = class _ItdAccounts {
6733
7235
  try {
6734
7236
  const errors = [];
6735
7237
  const closing = await Promise.allSettled([
6736
- client.close(),
7238
+ client.dispose(),
6737
7239
  storageControl?.drain() ?? Promise.resolve()
6738
7240
  ]);
6739
7241
  for (const result of closing) {
@@ -6761,14 +7263,64 @@ var ItdAccounts = class _ItdAccounts {
6761
7263
  * ```
6762
7264
  */
6763
7265
  use(plugin) {
6764
- validatePluginDefinition(plugin);
6765
- if (this.#plugins.some((added) => added.name === plugin?.name)) {
6766
- 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
+ );
6767
7270
  }
6768
- for (const client of this.#clients.values()) client.use(plugin);
6769
- 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);
6770
7289
  return this;
6771
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
+ }
6772
7324
  /**
6773
7325
  * Подписывается на события авторизации всех аккаунтов сразу.
6774
7326
  *
@@ -6806,16 +7358,32 @@ var ItdAccounts = class _ItdAccounts {
6806
7358
  * ```ts
6807
7359
  * await using accounts = new ItdAccounts({ storage });
6808
7360
  * // …работа…
6809
- * // close() вызовется сам на выходе из блока
7361
+ * // dispose() вызовется сам на выходе из блока
6810
7362
  * ```
6811
7363
  */
6812
7364
  async close() {
6813
7365
  await Promise.all([...this.#clients.values()].map((client) => client.close()));
6814
7366
  this.#queues?.stop();
6815
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
+ }
6816
7384
  /** Позволяет использовать контейнер с `await using`. */
6817
7385
  [Symbol.asyncDispose]() {
6818
- return this.close();
7386
+ return this.dispose();
6819
7387
  }
6820
7388
  static {
6821
7389
  if (typeof Symbol.asyncDispose !== "symbol") {
@@ -6841,6 +7409,12 @@ var ItdAccounts = class _ItdAccounts {
6841
7409
  storage
6842
7410
  };
6843
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
+ }
6844
7418
  /** Ретранслирует события клиента наружу, добавляя к ним имя аккаунта. */
6845
7419
  #forwardEvents(account, client) {
6846
7420
  return [
@@ -7295,5 +7869,5 @@ exports.scopedTokenStorage = scopedTokenStorage;
7295
7869
  exports.statusDays = statusDays;
7296
7870
  exports.toDate = toDate;
7297
7871
  exports.utcStampToIso = utcStampToIso;
7298
- //# sourceMappingURL=chunk-BN4AC3DP.cjs.map
7299
- //# sourceMappingURL=chunk-BN4AC3DP.cjs.map
7872
+ //# sourceMappingURL=chunk-73CISRBG.cjs.map
7873
+ //# sourceMappingURL=chunk-73CISRBG.cjs.map