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.
- package/README.md +8 -4
- package/dist/{chunk-SMV7TF5P.js → chunk-6FB4HTKH.js} +768 -194
- package/dist/chunk-6FB4HTKH.js.map +1 -0
- package/dist/{chunk-BN4AC3DP.cjs → chunk-73CISRBG.cjs} +768 -194
- package/dist/chunk-73CISRBG.cjs.map +1 -0
- package/dist/{index-CSjDNGCE.d.cts → index-BZF4K90s.d.cts} +169 -29
- package/dist/{index-CSjDNGCE.d.ts → index-BZF4K90s.d.ts} +169 -29
- package/dist/index.cjs +109 -109
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.cjs +112 -112
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/guides/README.md +2 -0
- package/guides/multi-accounts/README.md +2 -1
- package/guides/plugins/README.md +93 -2
- package/guides/reference/README.md +67 -0
- package/guides/reference/accounts.md +101 -0
- package/guides/reference/auth.md +141 -0
- package/guides/reference/builders.md +135 -0
- package/guides/reference/client.md +184 -0
- package/guides/reference/comments.md +58 -0
- package/guides/reference/discovery.md +81 -0
- package/guides/reference/enums.md +103 -0
- package/guides/reference/errors.md +107 -0
- package/guides/reference/files.md +73 -0
- package/guides/reference/models.md +448 -0
- package/guides/reference/notifications.md +77 -0
- package/guides/reference/pagination.md +82 -0
- package/guides/reference/platform.md +47 -0
- package/guides/reference/posts.md +157 -0
- package/guides/reference/realtime.md +78 -0
- package/guides/reference/reports.md +28 -0
- package/guides/reference/subscription.md +41 -0
- package/guides/reference/users.md +146 -0
- package/guides/reference/verification.md +24 -0
- package/package.json +1 -1
- package/dist/chunk-BN4AC3DP.cjs.map +0 -1
- 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
|
|
677
|
+
function readTokenIdentity(token) {
|
|
678
678
|
try {
|
|
679
679
|
const payload = token.split(".")[1];
|
|
680
|
-
if (!payload) return
|
|
680
|
+
if (!payload) return {};
|
|
681
681
|
const parsed = JSON.parse(decodeBase64Url(payload));
|
|
682
|
-
if (typeof parsed !== "object" || parsed === null) return
|
|
683
|
-
const
|
|
684
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
|
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
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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
|
|
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
|
|
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
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
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
|
|
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
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
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
|
-
|
|
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
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
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
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
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
|
|
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
|
-
* //
|
|
6840
|
+
* // dispose() вызовется сам на выходе из блока
|
|
6357
6841
|
* ```
|
|
6358
6842
|
*/
|
|
6359
6843
|
async close() {
|
|
6360
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
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
|
-
|
|
6769
|
-
this.#
|
|
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
|
-
* //
|
|
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.
|
|
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-
|
|
7299
|
-
//# sourceMappingURL=chunk-
|
|
7872
|
+
//# sourceMappingURL=chunk-73CISRBG.cjs.map
|
|
7873
|
+
//# sourceMappingURL=chunk-73CISRBG.cjs.map
|