@media-engine/core 0.1.0 → 0.1.1

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 (75) hide show
  1. package/README.md +27 -268
  2. package/README.ru.md +54 -0
  3. package/dist/cache/memory.d.ts +5 -0
  4. package/dist/cache/memory.js +40 -2
  5. package/dist/cache/memory.js.map +1 -1
  6. package/dist/cache/types.d.ts +2 -0
  7. package/dist/engine/availability.d.ts +5 -0
  8. package/dist/engine/availability.js +129 -0
  9. package/dist/engine/availability.js.map +1 -0
  10. package/dist/engine/circuit-breaker.d.ts +36 -0
  11. package/dist/engine/circuit-breaker.js +163 -0
  12. package/dist/engine/circuit-breaker.js.map +1 -0
  13. package/dist/engine/concurrency-limiter.d.ts +13 -0
  14. package/dist/engine/concurrency-limiter.js +109 -0
  15. package/dist/engine/concurrency-limiter.js.map +1 -0
  16. package/dist/engine/engine.d.ts +7 -2
  17. package/dist/engine/engine.js +335 -848
  18. package/dist/engine/engine.js.map +1 -1
  19. package/dist/engine/in-flight.d.ts +4 -0
  20. package/dist/engine/in-flight.js +25 -0
  21. package/dist/engine/in-flight.js.map +1 -0
  22. package/dist/engine/provider-calls.d.ts +42 -0
  23. package/dist/engine/provider-calls.js +212 -0
  24. package/dist/engine/provider-calls.js.map +1 -0
  25. package/dist/engine/query.d.ts +21 -0
  26. package/dist/engine/query.js +241 -0
  27. package/dist/engine/query.js.map +1 -0
  28. package/dist/engine/response-meta.d.ts +13 -0
  29. package/dist/engine/response-meta.js +26 -0
  30. package/dist/engine/response-meta.js.map +1 -0
  31. package/dist/engine/runtime.d.ts +3 -0
  32. package/dist/engine/runtime.js +29 -0
  33. package/dist/engine/runtime.js.map +1 -0
  34. package/dist/engine/search-enrichment.d.ts +28 -0
  35. package/dist/engine/search-enrichment.js +90 -0
  36. package/dist/engine/search-enrichment.js.map +1 -0
  37. package/dist/engine/stale-fallback.d.ts +11 -0
  38. package/dist/engine/stale-fallback.js +60 -0
  39. package/dist/engine/stale-fallback.js.map +1 -0
  40. package/dist/engine/timeout-budget.d.ts +7 -0
  41. package/dist/engine/timeout-budget.js +31 -0
  42. package/dist/engine/timeout-budget.js.map +1 -0
  43. package/dist/engine/types.d.ts +30 -0
  44. package/dist/merge/details-identity.d.ts +5 -0
  45. package/dist/merge/details-identity.js +63 -0
  46. package/dist/merge/details-identity.js.map +1 -0
  47. package/dist/merge/fields.d.ts +25 -0
  48. package/dist/merge/fields.js +364 -0
  49. package/dist/merge/fields.js.map +1 -0
  50. package/dist/merge/grouping.d.ts +3 -0
  51. package/dist/merge/grouping.js +143 -0
  52. package/dist/merge/grouping.js.map +1 -0
  53. package/dist/merge/identity.d.ts +5 -0
  54. package/dist/merge/identity.js +21 -0
  55. package/dist/merge/identity.js.map +1 -0
  56. package/dist/merge/internal.d.ts +22 -0
  57. package/dist/merge/internal.js +15 -0
  58. package/dist/merge/internal.js.map +1 -0
  59. package/dist/merge/media-type.d.ts +5 -0
  60. package/dist/merge/media-type.js +15 -0
  61. package/dist/merge/media-type.js.map +1 -0
  62. package/dist/merge/priority.d.ts +9 -0
  63. package/dist/merge/priority.js +61 -0
  64. package/dist/merge/priority.js.map +1 -0
  65. package/dist/merge/scoring.d.ts +5 -0
  66. package/dist/merge/scoring.js +260 -0
  67. package/dist/merge/scoring.js.map +1 -0
  68. package/dist/merge/strategy.js +14 -943
  69. package/dist/merge/strategy.js.map +1 -1
  70. package/dist/merge/title.d.ts +4 -0
  71. package/dist/merge/title.js +22 -0
  72. package/dist/merge/title.js.map +1 -0
  73. package/dist/providers/types.d.ts +1 -0
  74. package/dist/response/types.d.ts +1 -0
  75. package/package.json +7 -3
@@ -0,0 +1,241 @@
1
+ import { MediaEngineError } from "../errors/index.js";
2
+ // Top-level public external ID shortcuts supported by engine queries.
3
+ // Верхнеуровневые публичные сокращения внешних ID, поддерживаемые query движка.
4
+ export const EXTERNAL_ID_SHORTCUTS = [
5
+ "imdb",
6
+ "tmdb",
7
+ "kinopoisk",
8
+ "shikimori",
9
+ "myAnimeList",
10
+ "aniList",
11
+ ];
12
+ const SEARCH_FALLBACK_MIN_TOKENS = 3;
13
+ const SEARCH_FALLBACK_MIN_LAST_TOKEN_LENGTH = 4;
14
+ const SEARCH_JOINED_FALLBACK_MIN_LENGTH = 6;
15
+ const SEARCH_JOINED_FALLBACK_MAX_LENGTH = 8;
16
+ const SEARCH_JOINED_FALLBACK_MIN_PART_LENGTH = 3;
17
+ const MAX_SEARCH_LIMIT = 100;
18
+ const MAX_PROVIDER_SEARCH_LIMIT = 100;
19
+ // Normalizes top-level external ID shortcuts into the ids object.
20
+ // Нормализует верхнеуровневые сокращения внешних ID в объект ids.
21
+ export function normalizeSearchQuery(query) {
22
+ const ids = { ...(query.ids ?? {}) };
23
+ for (const key of EXTERNAL_ID_SHORTCUTS) {
24
+ const value = query[key];
25
+ if (value) {
26
+ ids[key] = value;
27
+ }
28
+ }
29
+ return {
30
+ ...query,
31
+ title: query.title?.trim(),
32
+ ids: hasExternalIds(ids) ? ids : undefined,
33
+ };
34
+ }
35
+ // Infers a provider lookup language only when the caller did not specify one.
36
+ // Определяет язык provider lookup только если caller не передал его явно.
37
+ export function inferTitleLanguage(title) {
38
+ if (!title)
39
+ return undefined;
40
+ if (/[а-яё]/iu.test(title))
41
+ return "ru";
42
+ if (/[\u3040-\u30ff\u3400-\u9fff]/u.test(title))
43
+ return "ja";
44
+ return /[a-z]/iu.test(title) ? "en" : undefined;
45
+ }
46
+ // Normalizes top-level external ID shortcuts into a details ids object.
47
+ // Нормализует верхнеуровневые сокращения внешних ID в объект ids для details.
48
+ export function normalizeDetailsQuery(query) {
49
+ const ids = { ...(query.ids ?? {}) };
50
+ for (const key of EXTERNAL_ID_SHORTCUTS) {
51
+ const value = query[key];
52
+ if (value) {
53
+ ids[key] = value;
54
+ }
55
+ }
56
+ return {
57
+ ...query,
58
+ ids: hasExternalIds(ids) ? ids : undefined,
59
+ };
60
+ }
61
+ // Normalizes top-level external ID shortcuts into a streaming ids object.
62
+ // Нормализует верхнеуровневые сокращения внешних ID в объект ids для streaming.
63
+ export function normalizeStreamQuery(query) {
64
+ const ids = { ...(query.ids ?? {}) };
65
+ const providers = query.providers?.map((provider) => provider.trim()).filter(Boolean);
66
+ const language = query.language?.trim();
67
+ for (const key of EXTERNAL_ID_SHORTCUTS) {
68
+ const value = query[key];
69
+ if (value) {
70
+ ids[key] = value;
71
+ }
72
+ }
73
+ return {
74
+ ...query,
75
+ title: query.title?.trim(),
76
+ ...(hasExternalIds(ids) ? { ids } : {}),
77
+ ...(providers && providers.length > 0 ? { providers } : {}),
78
+ ...(language ? { language } : {}),
79
+ };
80
+ }
81
+ // Validates that a search query has at least one supported lookup input.
82
+ // Проверяет, что search query содержит хотя бы один поддерживаемый вход для поиска.
83
+ export function validateSearchQuery(query) {
84
+ if (query.limit !== undefined &&
85
+ (!Number.isInteger(query.limit) || query.limit < 0 || query.limit > MAX_SEARCH_LIMIT)) {
86
+ throw new MediaEngineError({
87
+ code: "INVALID_QUERY",
88
+ message: `Search query limit must be an integer between 0 and ${MAX_SEARCH_LIMIT}.`,
89
+ });
90
+ }
91
+ if (query.title || hasExternalIds(query.ids)) {
92
+ return;
93
+ }
94
+ throw new MediaEngineError({
95
+ code: "INVALID_QUERY",
96
+ message: "Search query must include title or external ids.",
97
+ });
98
+ }
99
+ // Validates that a details query has at least one supported lookup input.
100
+ // Проверяет, что details query содержит хотя бы один поддерживаемый вход для поиска.
101
+ export function validateDetailsQuery(query) {
102
+ if (query.id?.trim() || hasExternalIds(query.ids)) {
103
+ return;
104
+ }
105
+ throw new MediaEngineError({
106
+ code: "INVALID_QUERY",
107
+ message: "Details query must include id or external ids.",
108
+ });
109
+ }
110
+ // Validates that a streaming query can identify a media item or episode.
111
+ // Проверяет, что streaming query может определить медиа или эпизод.
112
+ export function validateStreamQuery(query) {
113
+ if (!query.type) {
114
+ throw new MediaEngineError({
115
+ code: "INVALID_QUERY",
116
+ message: "Stream query type is required.",
117
+ });
118
+ }
119
+ if ([query.year, query.seasonNumber, query.episodeNumber, query.absoluteEpisodeNumber].some((value) => value !== undefined && (!Number.isInteger(value) || value < 0))) {
120
+ throw new MediaEngineError({
121
+ code: "INVALID_QUERY",
122
+ message: "Stream query numeric fields must be non-negative integers.",
123
+ });
124
+ }
125
+ if (query.title || hasExternalIds(query.ids)) {
126
+ return;
127
+ }
128
+ throw new MediaEngineError({
129
+ code: "INVALID_QUERY",
130
+ message: "Stream query must include title or external ids.",
131
+ });
132
+ }
133
+ // Gives providers enough candidates so the engine can rank before applying the public limit.
134
+ // Дает провайдерам достаточно кандидатов, чтобы движок ранжировал до применения публичного limit.
135
+ export function createProviderSearchQuery(query) {
136
+ if (query.limit === undefined || query.limit === 0) {
137
+ return query;
138
+ }
139
+ return {
140
+ ...query,
141
+ limit: getProviderSearchLimit(query),
142
+ };
143
+ }
144
+ // Broadens an empty typo search or separates one likely joined compound title.
145
+ // Расширяет пустой поиск с опечаткой или разделяет вероятно слитное составное название.
146
+ export function createSearchFallbackQuery(query) {
147
+ if (!query.title || hasExternalIds(query.ids)) {
148
+ return undefined;
149
+ }
150
+ const title = query.title.trim();
151
+ const tokens = title.split(/\s+/);
152
+ const lastToken = tokens.at(-1);
153
+ if (tokens.length >= SEARCH_FALLBACK_MIN_TOKENS &&
154
+ lastToken &&
155
+ lastToken.length >= SEARCH_FALLBACK_MIN_LAST_TOKEN_LENGTH) {
156
+ return {
157
+ ...query,
158
+ title: tokens.slice(0, -1).join(" "),
159
+ };
160
+ }
161
+ const characters = [...title];
162
+ if (tokens.length !== 1 ||
163
+ characters.length < SEARCH_JOINED_FALLBACK_MIN_LENGTH ||
164
+ characters.length > SEARCH_JOINED_FALLBACK_MAX_LENGTH ||
165
+ !/^\p{Script=Cyrillic}+$/u.test(title)) {
166
+ return undefined;
167
+ }
168
+ const splitIndex = Math.floor(characters.length / 2);
169
+ if (splitIndex < SEARCH_JOINED_FALLBACK_MIN_PART_LENGTH ||
170
+ characters.length - splitIndex < SEARCH_JOINED_FALLBACK_MIN_PART_LENGTH) {
171
+ return undefined;
172
+ }
173
+ return {
174
+ ...query,
175
+ title: `${characters.slice(0, splitIndex).join("")} ${characters.slice(splitIndex).join("")}`,
176
+ };
177
+ }
178
+ // Adds fallback discoveries without duplicating the same provider item and its attribution.
179
+ // Добавляет fallback-результаты без дублирования item и атрибуции одного провайдера.
180
+ export function appendUniqueSearchResults(target, candidates) {
181
+ for (const candidate of candidates) {
182
+ const isDuplicate = target.some((existing) => existing.provider === candidate.provider &&
183
+ existing.item.type === candidate.item.type &&
184
+ existing.item.id === candidate.item.id);
185
+ if (!isDuplicate) {
186
+ target.push(candidate);
187
+ }
188
+ }
189
+ }
190
+ // Creates a stable cache key for a normalized search query.
191
+ // Создает стабильный cache key для нормализованного search query.
192
+ export function createSearchCacheKey(query) {
193
+ return `search:${JSON.stringify(sortObject(query))}`;
194
+ }
195
+ // Creates a stable cache key for a normalized details query.
196
+ // Создает стабильный cache key для нормализованного details query.
197
+ export function createDetailsCacheKey(query) {
198
+ return `details:${JSON.stringify(sortObject(query))}`;
199
+ }
200
+ // Creates a stable cache key for a normalized streaming query.
201
+ // Создает стабильный cache key для нормализованного streaming query.
202
+ export function createAvailabilityCacheKey(query) {
203
+ return `availability:${JSON.stringify(sortObject(query))}`;
204
+ }
205
+ // Checks whether an external ID object contains at least one ID.
206
+ // Проверяет, содержит ли объект внешних ID хотя бы один ID.
207
+ export function hasExternalIds(ids) {
208
+ return Boolean(ids && Object.values(ids).some((value) => Boolean(value)));
209
+ }
210
+ // Sorts object keys recursively for deterministic JSON cache keys.
211
+ // Рекурсивно сортирует ключи объекта для детерминированных JSON cache keys.
212
+ export function sortObject(value) {
213
+ if (Array.isArray(value)) {
214
+ return value.map(sortObject);
215
+ }
216
+ if (value && typeof value === "object") {
217
+ return Object.fromEntries(Object.entries(value)
218
+ .filter(([, entryValue]) => entryValue !== undefined)
219
+ .sort(([left], [right]) => left.localeCompare(right))
220
+ .map(([key, entryValue]) => [key, sortObject(entryValue)]));
221
+ }
222
+ return value;
223
+ }
224
+ // Expands broad short queries more because final ranking needs enough cross-provider candidates.
225
+ // Расширяет короткие широкие запросы сильнее, потому что финальному ranking нужны кандидаты разных провайдеров.
226
+ function getProviderSearchLimit(query) {
227
+ if (isBroadShortTitleSearch(query)) {
228
+ return Math.min(MAX_PROVIDER_SEARCH_LIMIT, Math.max(query.limit * 10, 50));
229
+ }
230
+ return Math.min(MAX_PROVIDER_SEARCH_LIMIT, Math.max(query.limit * 5, 10));
231
+ }
232
+ // Detects searches like "one" or "game" where popular canonical results may be deeper.
233
+ // Определяет поиски вроде "one" или "game", где популярные канонические результаты могут быть глубже.
234
+ function isBroadShortTitleSearch(query) {
235
+ if (query.type || hasExternalIds(query.ids)) {
236
+ return false;
237
+ }
238
+ const normalizedTitle = query.title?.trim().replace(/\s+/g, " ") ?? "";
239
+ return (normalizedTitle.length > 0 && normalizedTitle.length <= 4 && !normalizedTitle.includes(" "));
240
+ }
241
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/engine/query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAMtD,sEAAsE;AACtE,gFAAgF;AAChF,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,MAAM;IACN,MAAM;IACN,WAAW;IACX,WAAW;IACX,aAAa;IACb,SAAS;CACD,CAAC;AAEX,MAAM,0BAA0B,GAAG,CAAC,CAAC;AACrC,MAAM,qCAAqC,GAAG,CAAC,CAAC;AAChD,MAAM,iCAAiC,GAAG,CAAC,CAAC;AAC5C,MAAM,iCAAiC,GAAG,CAAC,CAAC;AAC5C,MAAM,sCAAsC,GAAG,CAAC,CAAC;AACjD,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAEtC,kEAAkE;AAClE,kEAAkE;AAClE,MAAM,UAAU,oBAAoB,CAAC,KAAkB;IACrD,MAAM,GAAG,GAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC;IAElD,KAAK,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAEzB,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,KAAK;QACR,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE;QAC1B,GAAG,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;KAC3C,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,0EAA0E;AAC1E,MAAM,UAAU,kBAAkB,CAAC,KAAyB;IAC1D,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,+BAA+B,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAO,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED,wEAAwE;AACxE,8EAA8E;AAC9E,MAAM,UAAU,qBAAqB,CAAC,KAAmB;IACvD,MAAM,GAAG,GAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC;IAElD,KAAK,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAEzB,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,KAAK;QACR,GAAG,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;KAC3C,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,gFAAgF;AAChF,MAAM,UAAU,oBAAoB,CAAC,KAAkB;IACrD,MAAM,GAAG,GAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC;IAClD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtF,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IAExC,KAAK,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAEzB,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,KAAK;QACR,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE;QAC1B,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,GAAG,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,oFAAoF;AACpF,MAAM,UAAU,mBAAmB,CAAC,KAAkB;IACpD,IACE,KAAK,CAAC,KAAK,KAAK,SAAS;QACzB,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,gBAAgB,CAAC,EACrF,CAAC;QACD,MAAM,IAAI,gBAAgB,CAAC;YACzB,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,uDAAuD,gBAAgB,GAAG;SACpF,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,MAAM,IAAI,gBAAgB,CAAC;QACzB,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,kDAAkD;KAC5D,CAAC,CAAC;AACL,CAAC;AAED,0EAA0E;AAC1E,qFAAqF;AACrF,MAAM,UAAU,oBAAoB,CAAC,KAAmB;IACtD,IAAI,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAClD,OAAO;IACT,CAAC;IAED,MAAM,IAAI,gBAAgB,CAAC;QACzB,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,gDAAgD;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,yEAAyE;AACzE,oEAAoE;AACpE,MAAM,UAAU,mBAAmB,CAAC,KAAkB;IACpD,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,IAAI,gBAAgB,CAAC;YACzB,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,gCAAgC;SAC1C,CAAC,CAAC;IACL,CAAC;IAED,IACE,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,qBAAqB,CAAC,CAAC,IAAI,CACrF,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAC1E,EACD,CAAC;QACD,MAAM,IAAI,gBAAgB,CAAC;YACzB,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,4DAA4D;SACtE,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,MAAM,IAAI,gBAAgB,CAAC;QACzB,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,kDAAkD;KAC5D,CAAC,CAAC;AACL,CAAC;AAED,6FAA6F;AAC7F,kGAAkG;AAClG,MAAM,UAAU,yBAAyB,CAAC,KAAkB;IAC1D,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;QACnD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO;QACL,GAAG,KAAK;QACR,KAAK,EAAE,sBAAsB,CAAC,KAAK,CAAC;KACrC,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,wFAAwF;AACxF,MAAM,UAAU,yBAAyB,CAAC,KAAkB;IAC1D,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACjC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhC,IACE,MAAM,CAAC,MAAM,IAAI,0BAA0B;QAC3C,SAAS;QACT,SAAS,CAAC,MAAM,IAAI,qCAAqC,EACzD,CAAC;QACD,OAAO;YACL,GAAG,KAAK;YACR,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;SACrC,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAE9B,IACE,MAAM,CAAC,MAAM,KAAK,CAAC;QACnB,UAAU,CAAC,MAAM,GAAG,iCAAiC;QACrD,UAAU,CAAC,MAAM,GAAG,iCAAiC;QACrD,CAAC,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC,EACtC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAErD,IACE,UAAU,GAAG,sCAAsC;QACnD,UAAU,CAAC,MAAM,GAAG,UAAU,GAAG,sCAAsC,EACvE,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO;QACL,GAAG,KAAK;QACR,KAAK,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;KAC9F,CAAC;AACJ,CAAC;AAED,4FAA4F;AAC5F,qFAAqF;AACrF,MAAM,UAAU,yBAAyB,CACvC,MAA8B,EAC9B,UAAkC;IAElC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAC7B,CAAC,QAAQ,EAAE,EAAE,CACX,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC,QAAQ;YACxC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,IAAI;YAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,SAAS,CAAC,IAAI,CAAC,EAAE,CACzC,CAAC;QAEF,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;AACH,CAAC;AAED,4DAA4D;AAC5D,kEAAkE;AAClE,MAAM,UAAU,oBAAoB,CAAC,KAAkB;IACrD,OAAO,UAAU,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;AACvD,CAAC;AAED,6DAA6D;AAC7D,mEAAmE;AACnE,MAAM,UAAU,qBAAqB,CAAC,KAAmB;IACvD,OAAO,WAAW,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,+DAA+D;AAC/D,qEAAqE;AACrE,MAAM,UAAU,0BAA0B,CAAC,KAAkB;IAC3D,OAAO,gBAAgB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED,iEAAiE;AACjE,4DAA4D;AAC5D,MAAM,UAAU,cAAc,CAAC,GAA4B;IACzD,OAAO,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,mEAAmE;AACnE,4EAA4E;AAC5E,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;aAClB,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC;aACpD,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;aACpD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iGAAiG;AACjG,gHAAgH;AAChH,SAAS,sBAAsB,CAAC,KAAkB;IAChD,IAAI,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC,GAAG,CAAC,yBAAyB,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAM,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,yBAAyB,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAM,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,uFAAuF;AACvF,sGAAsG;AACtG,SAAS,uBAAuB,CAAC,KAAkB;IACjD,IAAI,KAAK,CAAC,IAAI,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,eAAe,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IAEvE,OAAO,CACL,eAAe,CAAC,MAAM,GAAG,CAAC,IAAI,eAAe,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,CAC5F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { EngineWarning, ProviderFailure, ProviderTimingMeta, ResponseMeta } from "../response/index.js";
2
+ export interface ResponseMetaInput {
3
+ requested: string[];
4
+ successful: string[];
5
+ failed: ProviderFailure[];
6
+ warnings: EngineWarning[];
7
+ cached: boolean;
8
+ tookMs: number;
9
+ debug: boolean;
10
+ timings?: ProviderTimingMeta[];
11
+ }
12
+ export declare function createResponseMeta(input: ResponseMetaInput): ResponseMeta;
13
+ export declare function elapsedSince(startedAt: number): number;
@@ -0,0 +1,26 @@
1
+ // Creates public response metadata for engine calls.
2
+ // Создает публичные метаданные ответа для вызовов engine.
3
+ export function createResponseMeta(input) {
4
+ return {
5
+ providers: {
6
+ requested: input.requested,
7
+ successful: input.successful,
8
+ failed: input.failed,
9
+ },
10
+ cached: input.cached,
11
+ tookMs: input.tookMs,
12
+ warnings: input.warnings.length > 0 ? input.warnings : undefined,
13
+ debug: input.debug
14
+ ? {
15
+ providers: input.requested,
16
+ timings: input.timings ?? [],
17
+ }
18
+ : undefined,
19
+ };
20
+ }
21
+ // Returns elapsed milliseconds since a start timestamp.
22
+ // Возвращает количество миллисекунд, прошедших с начального timestamp.
23
+ export function elapsedSince(startedAt) {
24
+ return Date.now() - startedAt;
25
+ }
26
+ //# sourceMappingURL=response-meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-meta.js","sourceRoot":"","sources":["../../src/engine/response-meta.ts"],"names":[],"mappings":"AAoBA,qDAAqD;AACrD,0DAA0D;AAC1D,MAAM,UAAU,kBAAkB,CAAC,KAAwB;IACzD,OAAO;QACL,SAAS,EAAE;YACT,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;SACrB;QACD,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;QAChE,KAAK,EAAE,KAAK,CAAC,KAAK;YAChB,CAAC,CAAC;gBACE,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE;aAC7B;YACH,CAAC,CAAC,SAAS;KACd,CAAC;AACJ,CAAC;AAED,wDAAwD;AACxD,uEAAuE;AACvE,MAAM,UAAU,YAAY,CAAC,SAAiB;IAC5C,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;AAChC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { StreamingProvider } from "../streaming/index.js";
2
+ export declare function validateStreamingProviders(providers: StreamingProvider[]): StreamingProvider[];
3
+ export declare function resolveProviderTimeoutMs(providerName: string, timeoutMs: number | undefined, providerTimeouts: Readonly<Record<string, number>>): number | undefined;
@@ -0,0 +1,29 @@
1
+ // Validates streaming providers and rejects duplicate public names.
2
+ // Проверяет streaming-провайдеры и отклоняет дубли публичных имен.
3
+ export function validateStreamingProviders(providers) {
4
+ const names = new Set();
5
+ for (const provider of providers) {
6
+ const name = provider.name.trim();
7
+ if (!name) {
8
+ throw new Error("Streaming provider name is required.");
9
+ }
10
+ if (name !== provider.name) {
11
+ throw new Error(`Streaming provider name "${provider.name}" must not include leading or trailing whitespace.`);
12
+ }
13
+ if (names.has(name)) {
14
+ throw new Error(`Streaming provider "${name}" is already registered.`);
15
+ }
16
+ names.add(name);
17
+ }
18
+ return [...providers];
19
+ }
20
+ // Resolves a provider override without allowing it to exceed the global boundary.
21
+ // Выбирает override провайдера, не позволяя ему превысить глобальную границу.
22
+ export function resolveProviderTimeoutMs(providerName, timeoutMs, providerTimeouts) {
23
+ const providerTimeoutMs = providerTimeouts[providerName];
24
+ if (providerTimeoutMs === undefined) {
25
+ return timeoutMs;
26
+ }
27
+ return timeoutMs === undefined ? providerTimeoutMs : Math.min(timeoutMs, providerTimeoutMs);
28
+ }
29
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/engine/runtime.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,mEAAmE;AACnE,MAAM,UAAU,0BAA0B,CAAC,SAA8B;IACvE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAEhC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAElC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC1D,CAAC;QAED,IAAI,IAAI,KAAK,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CACb,4BAA4B,QAAQ,CAAC,IAAI,oDAAoD,CAC9F,CAAC;QACJ,CAAC;QAED,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,0BAA0B,CAAC,CAAC;QACzE,CAAC;QAED,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;AACxB,CAAC;AAED,kFAAkF;AAClF,8EAA8E;AAC9E,MAAM,UAAU,wBAAwB,CACtC,YAAoB,EACpB,SAA6B,EAC7B,gBAAkD;IAElD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAEzD,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;QACpC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;AAC9F,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { ExternalIds, Image } from "../media/index.js";
2
+ import type { MergeStrategy } from "../merge/index.js";
3
+ import type { ProviderRegistry } from "../providers/index.js";
4
+ import type { MediaSearchResult } from "../search/index.js";
5
+ import type { ProviderCircuitBreaker } from "./circuit-breaker.js";
6
+ import type { ProviderConcurrencyLimiter } from "./concurrency-limiter.js";
7
+ export interface SearchPosterLookupInput {
8
+ result: MediaSearchResult;
9
+ language: string | undefined;
10
+ excludedProviders: ReadonlySet<string>;
11
+ registry: ProviderRegistry;
12
+ mergeStrategy: MergeStrategy;
13
+ debug: boolean;
14
+ circuitBreaker?: ProviderCircuitBreaker | undefined;
15
+ concurrencyLimiter?: ProviderConcurrencyLimiter | undefined;
16
+ getProviderTimeoutMs(providerName: string): number | undefined;
17
+ }
18
+ export interface SearchPosterEnrichment {
19
+ ids: ExternalIds | undefined;
20
+ poster: Image | undefined;
21
+ }
22
+ export declare function needsSearchEnrichment(item: {
23
+ ratings?: unknown[];
24
+ description?: string;
25
+ poster?: unknown;
26
+ }): boolean;
27
+ export declare function loadSearchPoster(input: SearchPosterLookupInput): Promise<Image | undefined>;
28
+ export declare function applySearchPosterEnrichments(results: MediaSearchResult[], enrichments: SearchPosterEnrichment[]): MediaSearchResult[];
@@ -0,0 +1,90 @@
1
+ import { callTimedProviderDetails } from "./provider-calls.js";
2
+ import { EXTERNAL_ID_SHORTCUTS, hasExternalIds } from "./query.js";
3
+ const SEARCH_DETAILS_POSTER_ENRICHMENT_TIMEOUT_MS = 1_500;
4
+ // Enriches compact catalog hits when follow-up cards would otherwise choose different metadata.
5
+ // Обогащает compact catalog hits, чтобы search и details не выбирали разные metadata.
6
+ export function needsSearchEnrichment(item) {
7
+ return !item.ratings?.length || !item.description?.trim() || !item.poster;
8
+ }
9
+ // Loads only the canonical poster needed by search without blocking on a full details request.
10
+ // Загружает только канонический постер для search, не блокируя полный details-запрос.
11
+ export async function loadSearchPoster(input) {
12
+ if (!hasExternalIds(input.result.item.ids)) {
13
+ return undefined;
14
+ }
15
+ const query = {
16
+ type: input.result.item.type,
17
+ ids: input.result.item.ids,
18
+ language: input.language,
19
+ };
20
+ const searchProviders = new Set(input.result.sources.map((source) => source.provider));
21
+ const providers = input.registry
22
+ .selectDetailsProviders(query)
23
+ .filter((provider) => !input.excludedProviders.has(provider.name));
24
+ const reusablePosterProvider = providers.find((provider) => provider.searchPosterMatchesDetails === true &&
25
+ provider.name === input.result.item.poster?.source &&
26
+ searchProviders.has(provider.name));
27
+ const providersToLoad = reusablePosterProvider
28
+ ? providers.filter((provider) => provider.searchPosterMatchesDetails !== true || !searchProviders.has(provider.name))
29
+ : providers;
30
+ const outcomes = await Promise.all(providersToLoad.map((provider) => {
31
+ const providerTimeoutMs = input.getProviderTimeoutMs(provider.name);
32
+ return callTimedProviderDetails(provider, query, {
33
+ debug: input.debug,
34
+ language: input.language,
35
+ circuitBreaker: input.circuitBreaker,
36
+ concurrencyLimiter: input.concurrencyLimiter,
37
+ timeoutMs: providerTimeoutMs === undefined
38
+ ? SEARCH_DETAILS_POSTER_ENRICHMENT_TIMEOUT_MS
39
+ : Math.min(providerTimeoutMs, SEARCH_DETAILS_POSTER_ENRICHMENT_TIMEOUT_MS),
40
+ });
41
+ }));
42
+ const loadedResults = new Map(outcomes.flatMap((outcome) => outcome.failure || !outcome.result ? [] : [[outcome.provider, outcome.result]]));
43
+ const providerResults = providers.flatMap((provider) => {
44
+ if (provider.name === reusablePosterProvider?.name) {
45
+ return [
46
+ {
47
+ provider: reusablePosterProvider.name,
48
+ details: searchItemAsDetails(input.result.item),
49
+ },
50
+ ];
51
+ }
52
+ const loaded = loadedResults.get(provider.name);
53
+ return loaded ? [loaded] : [];
54
+ });
55
+ return input.mergeStrategy.mergeDetails(providerResults, {
56
+ query,
57
+ language: input.language,
58
+ debug: input.debug,
59
+ warnings: [],
60
+ })?.poster;
61
+ }
62
+ // Reuses only compact fields; every details-only field remains absent by construction.
63
+ // Переиспользует только compact-поля; details-only поля намеренно остаются пустыми.
64
+ function searchItemAsDetails(item) {
65
+ switch (item.type) {
66
+ case "movie":
67
+ return { ...item, type: "movie" };
68
+ case "series":
69
+ return { ...item, type: "series" };
70
+ case "anime":
71
+ return { ...item, type: "anime" };
72
+ }
73
+ }
74
+ // Applies canonical poster enrichments to matching search results.
75
+ // Применяет canonical poster enrichment к совпадающим search results.
76
+ export function applySearchPosterEnrichments(results, enrichments) {
77
+ return results.map((result) => {
78
+ const poster = enrichments.find((enrichment) => enrichment.poster && hasSharedExternalId(result.item.ids, enrichment.ids))?.poster;
79
+ return poster ? { ...result, item: { ...result.item, poster } } : result;
80
+ });
81
+ }
82
+ // Checks whether two normalized media identities share at least one exact external ID.
83
+ // Проверяет, совпадает ли у двух нормализованных media identity хотя бы один внешний ID.
84
+ function hasSharedExternalId(left, right) {
85
+ if (!left || !right) {
86
+ return false;
87
+ }
88
+ return EXTERNAL_ID_SHORTCUTS.some((key) => Boolean(left[key] && left[key] === right[key]));
89
+ }
90
+ //# sourceMappingURL=search-enrichment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-enrichment.js","sourceRoot":"","sources":["../../src/engine/search-enrichment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ResponseMeta } from "../response/index.js";
2
+ interface ResponseWithMeta {
3
+ meta: ResponseMeta;
4
+ }
5
+ export interface StaleFallbackInput<T extends ResponseWithMeta> {
6
+ stale: T | undefined;
7
+ pending: Promise<T>;
8
+ tookMs: () => number;
9
+ }
10
+ export declare function loadWithStaleFallback<T extends ResponseWithMeta>(input: StaleFallbackInput<T>): Promise<T>;
11
+ export {};
@@ -0,0 +1,60 @@
1
+ import { MediaEngineError } from "../errors/index.js";
2
+ // Returns stale metadata only when every current provider failure is retryable.
3
+ // Возвращает устаревшие метаданные только при retryable-сбое всех текущих провайдеров.
4
+ export async function loadWithStaleFallback(input) {
5
+ try {
6
+ return await input.pending;
7
+ }
8
+ catch (error) {
9
+ const failed = getRetryableProviderFailures(error);
10
+ if (!input.stale || !failed) {
11
+ throw error;
12
+ }
13
+ const response = structuredClone(input.stale);
14
+ response.meta = {
15
+ providers: {
16
+ requested: failed.map((failure) => failure.provider),
17
+ successful: [],
18
+ failed,
19
+ },
20
+ cached: true,
21
+ stale: true,
22
+ tookMs: input.tookMs(),
23
+ warnings: [
24
+ {
25
+ code: "STALE_CACHE_FALLBACK",
26
+ message: "Returned stale cached data because all selected providers failed retryably.",
27
+ },
28
+ ],
29
+ debug: response.meta.debug
30
+ ? {
31
+ providers: failed.map((failure) => failure.provider),
32
+ timings: [],
33
+ }
34
+ : undefined,
35
+ };
36
+ return response;
37
+ }
38
+ }
39
+ function getRetryableProviderFailures(error) {
40
+ if (!(error instanceof MediaEngineError) || error.code !== "PROVIDER_ERROR") {
41
+ return undefined;
42
+ }
43
+ const cause = error.cause;
44
+ const failed = cause?.failed;
45
+ if (!Array.isArray(failed) || failed.length === 0 || !failed.every(isRetryableProviderFailure)) {
46
+ return undefined;
47
+ }
48
+ return failed;
49
+ }
50
+ function isRetryableProviderFailure(value) {
51
+ if (typeof value !== "object" || value === null) {
52
+ return false;
53
+ }
54
+ const failure = value;
55
+ return (typeof failure.provider === "string" &&
56
+ typeof failure.code === "string" &&
57
+ failure.retryable === true &&
58
+ typeof failure.message === "string");
59
+ }
60
+ //# sourceMappingURL=stale-fallback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stale-fallback.js","sourceRoot":"","sources":["../../src/engine/stale-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAatD,gFAAgF;AAChF,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,KAA4B;IAE5B,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,OAAO,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,4BAA4B,CAAC,KAAK,CAAC,CAAC;QAEnD,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;YAC5B,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC9C,QAAQ,CAAC,IAAI,GAAG;YACd,SAAS,EAAE;gBACT,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACpD,UAAU,EAAE,EAAE;gBACd,MAAM;aACP;YACD,MAAM,EAAE,IAAI;YACZ,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE;YACtB,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,sBAAsB;oBAC5B,OAAO,EAAE,6EAA6E;iBACvF;aACF;YACD,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK;gBACxB,CAAC,CAAC;oBACE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;oBACpD,OAAO,EAAE,EAAE;iBACZ;gBACH,CAAC,CAAC,SAAS;SACd,CAAC;QAEF,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,4BAA4B,CAAC,KAAc;IAClD,IAAI,CAAC,CAAC,KAAK,YAAY,gBAAgB,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QAC5E,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAyC,CAAC;IAC9D,MAAM,MAAM,GAAG,KAAK,EAAE,MAAM,CAAC;IAE7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B,CAAC,EAAE,CAAC;QAC/F,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,0BAA0B,CAAC,KAAc;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,KAAiC,CAAC;IAClD,OAAO,CACL,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ;QACpC,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ;QAChC,OAAO,CAAC,SAAS,KAAK,IAAI;QAC1B,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,CACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare class ProviderTimeoutBudget {
2
+ private readonly resolveTimeoutMs;
3
+ private readonly now;
4
+ private readonly deadlines;
5
+ constructor(resolveTimeoutMs: (provider: string) => number | undefined, now?: () => number);
6
+ getRemainingMs(provider: string, callLimitMs?: number): number | undefined;
7
+ }
@@ -0,0 +1,31 @@
1
+ // Shares one timeout deadline across all calls to a provider in one engine operation.
2
+ // Делит один timeout deadline между всеми вызовами провайдера в одной операции движка.
3
+ export class ProviderTimeoutBudget {
4
+ resolveTimeoutMs;
5
+ now;
6
+ deadlines = new Map();
7
+ constructor(resolveTimeoutMs, now = Date.now) {
8
+ this.resolveTimeoutMs = resolveTimeoutMs;
9
+ this.now = now;
10
+ }
11
+ getRemainingMs(provider, callLimitMs) {
12
+ const now = this.now();
13
+ const existingDeadline = this.deadlines.get(provider);
14
+ if (existingDeadline !== undefined) {
15
+ return applyCallLimit(Math.max(0, existingDeadline - now), callLimitMs);
16
+ }
17
+ const timeoutMs = this.resolveTimeoutMs(provider);
18
+ const initialBudget = applyCallLimit(timeoutMs, callLimitMs);
19
+ if (initialBudget !== undefined) {
20
+ this.deadlines.set(provider, now + initialBudget);
21
+ }
22
+ return initialBudget;
23
+ }
24
+ }
25
+ function applyCallLimit(timeoutMs, callLimitMs) {
26
+ if (timeoutMs === undefined) {
27
+ return callLimitMs;
28
+ }
29
+ return callLimitMs === undefined ? timeoutMs : Math.min(timeoutMs, callLimitMs);
30
+ }
31
+ //# sourceMappingURL=timeout-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout-budget.js","sourceRoot":"","sources":["../../src/engine/timeout-budget.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,uFAAuF;AACvF,MAAM,OAAO,qBAAqB;IAIb;IACA;IAJF,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEvD,YACmB,gBAA0D,EAC1D,MAAoB,IAAI,CAAC,GAAG;QAD5B,qBAAgB,GAAhB,gBAAgB,CAA0C;QAC1D,QAAG,GAAH,GAAG,CAAyB;IAC5C,CAAC;IAEJ,cAAc,CAAC,QAAgB,EAAE,WAAoB;QACnD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAEtD,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,gBAAgB,GAAG,GAAG,CAAC,EAAE,WAAW,CAAC,CAAC;QAC1E,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,aAAa,GAAG,cAAc,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QAE7D,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,GAAG,aAAa,CAAC,CAAC;QACpD,CAAC;QAED,OAAO,aAAa,CAAC;IACvB,CAAC;CACF;AAED,SAAS,cAAc,CACrB,SAA6B,EAC7B,WAA+B;IAE/B,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,OAAO,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAClF,CAAC"}
@@ -2,6 +2,34 @@ import type { Cache } from "../cache/index.js";
2
2
  import type { MergeStrategy } from "../merge/index.js";
3
3
  import type { MediaProvider } from "../providers/index.js";
4
4
  import type { StreamingProvider } from "../streaming/index.js";
5
+ export interface CircuitBreakerOptions {
6
+ failureThreshold?: number;
7
+ recoveryTimeoutMs?: number;
8
+ }
9
+ export interface ProviderConcurrencyOptions {
10
+ defaultMaxConcurrent?: number;
11
+ maxQueueSize?: number;
12
+ providerLimits?: Readonly<Record<string, number>>;
13
+ }
14
+ export interface ProviderHealthStatus {
15
+ provider: string;
16
+ kind: "metadata" | "streaming";
17
+ circuitState: "closed" | "open" | "half-open" | "disabled";
18
+ consecutiveFailures: number;
19
+ totalRequests: number;
20
+ totalSuccesses: number;
21
+ totalFailures: number;
22
+ lastSuccessAt?: string;
23
+ lastFailureAt?: string;
24
+ lastFailureCode?: string;
25
+ failureCounts?: {
26
+ timeout: number;
27
+ rateLimited: number;
28
+ unavailable: number;
29
+ other: number;
30
+ };
31
+ retryAfterMs?: number;
32
+ }
5
33
  export interface MediaEngineOptions {
6
34
  providers?: MediaProvider[];
7
35
  streamingProviders?: StreamingProvider[];
@@ -9,5 +37,7 @@ export interface MediaEngineOptions {
9
37
  mergeStrategy?: MergeStrategy;
10
38
  timeoutMs?: number;
11
39
  providerTimeouts?: Readonly<Record<string, number>>;
40
+ circuitBreaker?: CircuitBreakerOptions | false;
41
+ providerConcurrency?: ProviderConcurrencyOptions | false;
12
42
  debug?: boolean;
13
43
  }
@@ -0,0 +1,5 @@
1
+ import type { MediaType } from "../media/index.js";
2
+ import type { DetailsEntry } from "./internal.js";
3
+ import type { MergeContext } from "./types.js";
4
+ export declare function filterDetailsEntriesByIdentity(entries: DetailsEntry[], context: MergeContext): DetailsEntry[];
5
+ export declare function warnDetailsTypeConflicts(entries: DetailsEntry[], context: MergeContext, selectedType?: MediaType): void;