@media-engine/core 0.1.0 → 1.0.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 +40 -255
- package/README.ru.md +80 -0
- package/dist/cache/memory.d.ts +5 -0
- package/dist/cache/memory.js +43 -3
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/types.d.ts +2 -0
- package/dist/details/types.d.ts +6 -0
- package/dist/engine/availability.d.ts +6 -0
- package/dist/engine/availability.js +137 -0
- package/dist/engine/availability.js.map +1 -0
- package/dist/engine/circuit-breaker.d.ts +37 -0
- package/dist/engine/circuit-breaker.js +174 -0
- package/dist/engine/circuit-breaker.js.map +1 -0
- package/dist/engine/concurrency-limiter.d.ts +13 -0
- package/dist/engine/concurrency-limiter.js +109 -0
- package/dist/engine/concurrency-limiter.js.map +1 -0
- package/dist/engine/engine.d.ts +15 -5
- package/dist/engine/engine.js +510 -848
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/in-flight.d.ts +15 -0
- package/dist/engine/in-flight.js +80 -0
- package/dist/engine/in-flight.js.map +1 -0
- package/dist/engine/operation.d.ts +7 -0
- package/dist/engine/operation.js +49 -0
- package/dist/engine/operation.js.map +1 -0
- package/dist/engine/provider-calls.d.ts +52 -0
- package/dist/engine/provider-calls.js +262 -0
- package/dist/engine/provider-calls.js.map +1 -0
- package/dist/engine/query.d.ts +26 -0
- package/dist/engine/query.js +430 -0
- package/dist/engine/query.js.map +1 -0
- package/dist/engine/response-meta.d.ts +15 -0
- package/dist/engine/response-meta.js +33 -0
- package/dist/engine/response-meta.js.map +1 -0
- package/dist/engine/runtime.d.ts +5 -0
- package/dist/engine/runtime.js +48 -0
- package/dist/engine/runtime.js.map +1 -0
- package/dist/engine/search-discovery.d.ts +3 -0
- package/dist/engine/search-discovery.js +35 -0
- package/dist/engine/search-discovery.js.map +1 -0
- package/dist/engine/search-enrichment-shared.d.ts +23 -0
- package/dist/engine/search-enrichment-shared.js +68 -0
- package/dist/engine/search-enrichment-shared.js.map +1 -0
- package/dist/engine/search-enrichment.d.ts +42 -0
- package/dist/engine/search-enrichment.js +150 -0
- package/dist/engine/search-enrichment.js.map +1 -0
- package/dist/engine/search-identity-snapshot.d.ts +15 -0
- package/dist/engine/search-identity-snapshot.js +101 -0
- package/dist/engine/search-identity-snapshot.js.map +1 -0
- package/dist/engine/search-outcomes.d.ts +32 -0
- package/dist/engine/search-outcomes.js +118 -0
- package/dist/engine/search-outcomes.js.map +1 -0
- package/dist/engine/search-poster-enrichment.d.ts +31 -0
- package/dist/engine/search-poster-enrichment.js +93 -0
- package/dist/engine/search-poster-enrichment.js.map +1 -0
- package/dist/engine/search-result-freeze.d.ts +13 -0
- package/dist/engine/search-result-freeze.js +155 -0
- package/dist/engine/search-result-freeze.js.map +1 -0
- package/dist/engine/stale-fallback.d.ts +11 -0
- package/dist/engine/stale-fallback.js +60 -0
- package/dist/engine/stale-fallback.js.map +1 -0
- package/dist/engine/timeout-budget.d.ts +7 -0
- package/dist/engine/timeout-budget.js +31 -0
- package/dist/engine/timeout-budget.js.map +1 -0
- package/dist/engine/torrents.d.ts +22 -0
- package/dist/engine/torrents.js +188 -0
- package/dist/engine/torrents.js.map +1 -0
- package/dist/engine/types.d.ts +35 -0
- package/dist/errors/types.d.ts +1 -1
- package/dist/errors/types.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/media/types.d.ts +1 -1
- package/dist/merge/details-identity.d.ts +5 -0
- package/dist/merge/details-identity.js +63 -0
- package/dist/merge/details-identity.js.map +1 -0
- package/dist/merge/diversity.d.ts +15 -0
- package/dist/merge/diversity.js +52 -0
- package/dist/merge/diversity.js.map +1 -0
- package/dist/merge/fields.d.ts +25 -0
- package/dist/merge/fields.js +364 -0
- package/dist/merge/fields.js.map +1 -0
- package/dist/merge/grouping.d.ts +3 -0
- package/dist/merge/grouping.js +143 -0
- package/dist/merge/grouping.js.map +1 -0
- package/dist/merge/identity.d.ts +5 -0
- package/dist/merge/identity.js +21 -0
- package/dist/merge/identity.js.map +1 -0
- package/dist/merge/internal.d.ts +22 -0
- package/dist/merge/internal.js +15 -0
- package/dist/merge/internal.js.map +1 -0
- package/dist/merge/media-type.d.ts +5 -0
- package/dist/merge/media-type.js +15 -0
- package/dist/merge/media-type.js.map +1 -0
- package/dist/merge/priority.d.ts +9 -0
- package/dist/merge/priority.js +63 -0
- package/dist/merge/priority.js.map +1 -0
- package/dist/merge/scoring.d.ts +15 -0
- package/dist/merge/scoring.js +364 -0
- package/dist/merge/scoring.js.map +1 -0
- package/dist/merge/strategy.js +66 -955
- package/dist/merge/strategy.js.map +1 -1
- package/dist/merge/title.d.ts +4 -0
- package/dist/merge/title.js +22 -0
- package/dist/merge/title.js.map +1 -0
- package/dist/providers/registry.d.ts +4 -2
- package/dist/providers/registry.js +11 -2
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +4 -0
- package/dist/response/types.d.ts +21 -0
- package/dist/search/types.d.ts +34 -0
- package/dist/testing/providers.js +2 -0
- package/dist/testing/providers.js.map +1 -1
- package/dist/torrent/index.d.ts +1 -0
- package/dist/torrent/index.js +2 -0
- package/dist/torrent/index.js.map +1 -0
- package/dist/torrent/types.d.ts +132 -0
- package/dist/torrent/types.js +2 -0
- package/dist/torrent/types.js.map +1 -0
- package/package.json +13 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { sortObject } from "./query.js";
|
|
2
|
+
const EXPIRING_AVAILABILITY_CACHE_SAFETY_MS = 1_000;
|
|
3
|
+
// Selects streaming providers that can answer the normalized stream query.
|
|
4
|
+
// Выбирает streaming-провайдеры, которые могут ответить на нормализованный stream query.
|
|
5
|
+
export function selectStreamingProviders(providers, query) {
|
|
6
|
+
return providers.filter((provider) => {
|
|
7
|
+
if (query.providers && !query.providers.includes(provider.name)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (!provider.capabilities.mediaTypes.includes(query.type)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (hasEpisodeQuery(query) && !provider.capabilities.lookup.byEpisode) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return (Boolean(query.title && provider.capabilities.lookup.byTitle) ||
|
|
17
|
+
hasSupportedExternalId(query.ids, provider.capabilities.lookup.byExternalIds));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Merges availability results without hiding provider attribution.
|
|
21
|
+
// Объединяет availability-результаты, не скрывая атрибуцию провайдеров.
|
|
22
|
+
export function mergeAvailabilityResults(query, results) {
|
|
23
|
+
return {
|
|
24
|
+
query,
|
|
25
|
+
item: results.find((result) => result.item)?.item,
|
|
26
|
+
episodes: mergeEpisodeAvailability(results),
|
|
27
|
+
options: uniqueBy(results.flatMap((result) => result.options), (option) => `${option.provider}:${option.id}`),
|
|
28
|
+
sourceProviders: uniqueBy(results.flatMap((result) => result.sourceProviders), (source) => createStreamingSourceKey(source)),
|
|
29
|
+
checkedAt: new Date().toISOString(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Keeps cached direct links from outliving the earliest advertised expiration.
|
|
33
|
+
// Не позволяет кешированным прямым ссылкам пережить ближайший заявленный срок действия.
|
|
34
|
+
export function createAvailabilityCacheOptions(availability) {
|
|
35
|
+
const expiresAtValues = [
|
|
36
|
+
...availability.options,
|
|
37
|
+
...(availability.episodes?.flatMap((episode) => episode.options) ?? []),
|
|
38
|
+
]
|
|
39
|
+
.map((option) => option.expiresAt)
|
|
40
|
+
.filter((value) => value !== undefined)
|
|
41
|
+
.map((value) => Date.parse(value))
|
|
42
|
+
.filter(Number.isFinite);
|
|
43
|
+
if (expiresAtValues.length === 0) {
|
|
44
|
+
return { staleTtlMs: 0 };
|
|
45
|
+
}
|
|
46
|
+
const earliestExpiration = Math.min(...expiresAtValues);
|
|
47
|
+
return {
|
|
48
|
+
ttlMs: Math.max(0, earliestExpiration - Date.now() - EXPIRING_AVAILABILITY_CACHE_SAFETY_MS),
|
|
49
|
+
staleTtlMs: 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Detects player options kept after validation could not reach a reliable conclusion.
|
|
53
|
+
// Находит player options, сохраненные после неопределенного результата validation.
|
|
54
|
+
export function hasUnknownStreamValidation(availability) {
|
|
55
|
+
return [
|
|
56
|
+
...availability.options,
|
|
57
|
+
...(availability.episodes?.flatMap((episode) => episode.options) ?? []),
|
|
58
|
+
].some((option) => option.availability === "unknown");
|
|
59
|
+
}
|
|
60
|
+
// Merges episode-level availability blocks by episode identity.
|
|
61
|
+
// Объединяет episode-level availability блоки по идентичности эпизода.
|
|
62
|
+
function mergeEpisodeAvailability(results) {
|
|
63
|
+
const episodesByKey = new Map();
|
|
64
|
+
for (const episode of results.flatMap((result) => [
|
|
65
|
+
...(result.episodes ?? []),
|
|
66
|
+
...createEpisodeAvailabilityFromOptions(result.options),
|
|
67
|
+
])) {
|
|
68
|
+
const key = createEpisodeKey(episode);
|
|
69
|
+
const existing = episodesByKey.get(key);
|
|
70
|
+
if (!existing) {
|
|
71
|
+
episodesByKey.set(key, {
|
|
72
|
+
seasonNumber: episode.seasonNumber,
|
|
73
|
+
episodeNumber: episode.episodeNumber,
|
|
74
|
+
absoluteEpisodeNumber: episode.absoluteEpisodeNumber,
|
|
75
|
+
title: episode.title,
|
|
76
|
+
options: uniqueBy(episode.options, (option) => `${option.provider}:${option.id}`),
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
existing.options = uniqueBy([...existing.options, ...episode.options], (option) => `${option.provider}:${option.id}`);
|
|
81
|
+
existing.title ??= episode.title;
|
|
82
|
+
}
|
|
83
|
+
return episodesByKey.size > 0 ? [...episodesByKey.values()] : undefined;
|
|
84
|
+
}
|
|
85
|
+
// Creates episode blocks from top-level options that carry episode identity.
|
|
86
|
+
// Создает episode blocks из top-level options, которые содержат идентичность эпизода.
|
|
87
|
+
function createEpisodeAvailabilityFromOptions(options) {
|
|
88
|
+
return options
|
|
89
|
+
.filter((option) => option.episode)
|
|
90
|
+
.map((option) => ({
|
|
91
|
+
seasonNumber: option.episode?.seasonNumber,
|
|
92
|
+
episodeNumber: option.episode?.episodeNumber,
|
|
93
|
+
absoluteEpisodeNumber: option.episode?.absoluteEpisodeNumber,
|
|
94
|
+
options: [option],
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
// Checks whether query ids overlap provider-supported external ID sources.
|
|
98
|
+
// Проверяет, пересекаются ли query ids с поддерживаемыми провайдером источниками ID.
|
|
99
|
+
function hasSupportedExternalId(ids, supportedSources) {
|
|
100
|
+
return Boolean(ids && supportedSources.some((source) => Boolean(ids[source])));
|
|
101
|
+
}
|
|
102
|
+
// Checks whether query targets a concrete episode.
|
|
103
|
+
// Проверяет, нацелен ли query на конкретный эпизод.
|
|
104
|
+
function hasEpisodeQuery(query) {
|
|
105
|
+
return (query.seasonNumber !== undefined ||
|
|
106
|
+
query.episodeNumber !== undefined ||
|
|
107
|
+
query.absoluteEpisodeNumber !== undefined);
|
|
108
|
+
}
|
|
109
|
+
// Creates a stable identity for an episode availability block.
|
|
110
|
+
// Создает стабильную идентичность для блока доступности эпизода.
|
|
111
|
+
function createEpisodeKey(episode) {
|
|
112
|
+
return [
|
|
113
|
+
episode.seasonNumber ?? "",
|
|
114
|
+
episode.episodeNumber ?? "",
|
|
115
|
+
episode.absoluteEpisodeNumber ?? "",
|
|
116
|
+
].join(":");
|
|
117
|
+
}
|
|
118
|
+
// Creates a stable identity for provider source attribution.
|
|
119
|
+
// Создает стабильную идентичность для атрибуции источника провайдера.
|
|
120
|
+
function createStreamingSourceKey(source) {
|
|
121
|
+
return `${source.provider}:${source.url ?? ""}:${JSON.stringify(sortObject(source.ids ?? {}))}`;
|
|
122
|
+
}
|
|
123
|
+
// Keeps the first value for each derived key.
|
|
124
|
+
// Оставляет первое значение для каждого вычисленного ключа.
|
|
125
|
+
function uniqueBy(values, getKey) {
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const unique = [];
|
|
128
|
+
for (const value of values) {
|
|
129
|
+
const key = getKey(value);
|
|
130
|
+
if (!seen.has(key)) {
|
|
131
|
+
seen.add(key);
|
|
132
|
+
unique.push(value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return unique;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=availability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability.js","sourceRoot":"","sources":["../../src/engine/availability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CircuitBreakerOptions } from "./types.js";
|
|
2
|
+
interface ProviderFailureCounts {
|
|
3
|
+
timeout: number;
|
|
4
|
+
rateLimited: number;
|
|
5
|
+
unavailable: number;
|
|
6
|
+
other: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CircuitBreakerSnapshot {
|
|
9
|
+
state: "closed" | "open" | "half-open";
|
|
10
|
+
consecutiveFailures: number;
|
|
11
|
+
totalRequests: number;
|
|
12
|
+
totalSuccesses: number;
|
|
13
|
+
totalFailures: number;
|
|
14
|
+
lastSuccessAt?: number;
|
|
15
|
+
lastFailureAt?: number;
|
|
16
|
+
lastFailureCode?: string;
|
|
17
|
+
failureCounts: ProviderFailureCounts;
|
|
18
|
+
retryAfterMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class ProviderCircuitBreaker {
|
|
21
|
+
private readonly failureThreshold;
|
|
22
|
+
private readonly recoveryTimeoutMs;
|
|
23
|
+
private readonly now;
|
|
24
|
+
private readonly states;
|
|
25
|
+
private readonly observations;
|
|
26
|
+
constructor(options?: CircuitBreakerOptions, now?: () => number);
|
|
27
|
+
run<T>(key: string, provider: string, operation: () => Promise<T>): Promise<T>;
|
|
28
|
+
private releaseCancelledProbe;
|
|
29
|
+
getSnapshot(key: string): CircuitBreakerSnapshot;
|
|
30
|
+
private acquire;
|
|
31
|
+
private recordFailure;
|
|
32
|
+
private recordRequest;
|
|
33
|
+
private recordSuccess;
|
|
34
|
+
private recordObservedFailure;
|
|
35
|
+
private getObservation;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { ProviderError } from "../errors/index.js";
|
|
2
|
+
import { isOperationCancelledError } from "./operation.js";
|
|
3
|
+
const DEFAULT_FAILURE_THRESHOLD = 3;
|
|
4
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 30_000;
|
|
5
|
+
// Tracks transient provider failures and allows one recovery probe after cooldown.
|
|
6
|
+
// Отслеживает временные сбои провайдера и разрешает одну пробу после cooldown.
|
|
7
|
+
export class ProviderCircuitBreaker {
|
|
8
|
+
failureThreshold;
|
|
9
|
+
recoveryTimeoutMs;
|
|
10
|
+
now;
|
|
11
|
+
states = new Map();
|
|
12
|
+
observations = new Map();
|
|
13
|
+
constructor(options = {}, now = Date.now) {
|
|
14
|
+
this.failureThreshold = validatePositiveInteger(options.failureThreshold, DEFAULT_FAILURE_THRESHOLD, "failureThreshold");
|
|
15
|
+
this.recoveryTimeoutMs = validateNonNegativeInteger(options.recoveryTimeoutMs, DEFAULT_RECOVERY_TIMEOUT_MS, "recoveryTimeoutMs");
|
|
16
|
+
this.now = now;
|
|
17
|
+
}
|
|
18
|
+
async run(key, provider, operation) {
|
|
19
|
+
this.acquire(key, provider);
|
|
20
|
+
this.recordRequest(key);
|
|
21
|
+
try {
|
|
22
|
+
const result = await operation();
|
|
23
|
+
this.states.delete(key);
|
|
24
|
+
this.recordSuccess(key);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (isOperationCancelledError(error)) {
|
|
29
|
+
this.releaseCancelledProbe(key);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
this.recordFailure(key, error);
|
|
33
|
+
this.recordObservedFailure(key, error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
releaseCancelledProbe(key) {
|
|
38
|
+
const state = this.states.get(key);
|
|
39
|
+
if (state?.probeInFlight) {
|
|
40
|
+
state.probeInFlight = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
getSnapshot(key) {
|
|
44
|
+
const now = this.now();
|
|
45
|
+
const state = this.states.get(key);
|
|
46
|
+
const observation = this.observations.get(key);
|
|
47
|
+
const circuitState = state?.probeInFlight
|
|
48
|
+
? "half-open"
|
|
49
|
+
: state?.openedAt !== undefined
|
|
50
|
+
? "open"
|
|
51
|
+
: "closed";
|
|
52
|
+
const retryAfterMs = state?.openedAt === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: Math.max(0, this.recoveryTimeoutMs - (now - state.openedAt));
|
|
55
|
+
return {
|
|
56
|
+
state: circuitState,
|
|
57
|
+
consecutiveFailures: state?.failures ?? 0,
|
|
58
|
+
totalRequests: observation?.totalRequests ?? 0,
|
|
59
|
+
totalSuccesses: observation?.totalSuccesses ?? 0,
|
|
60
|
+
totalFailures: observation?.totalFailures ?? 0,
|
|
61
|
+
lastSuccessAt: observation?.lastSuccessAt,
|
|
62
|
+
lastFailureAt: observation?.lastFailureAt,
|
|
63
|
+
lastFailureCode: observation?.lastFailureCode,
|
|
64
|
+
failureCounts: observation?.failureCounts ?? createEmptyFailureCounts(),
|
|
65
|
+
retryAfterMs,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
acquire(key, provider) {
|
|
69
|
+
const state = this.states.get(key);
|
|
70
|
+
if (state?.openedAt === undefined) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (this.now() - state.openedAt < this.recoveryTimeoutMs || state.probeInFlight) {
|
|
74
|
+
throw createOpenCircuitError(provider);
|
|
75
|
+
}
|
|
76
|
+
state.probeInFlight = true;
|
|
77
|
+
}
|
|
78
|
+
recordFailure(key, error) {
|
|
79
|
+
const state = this.states.get(key);
|
|
80
|
+
if (!(error instanceof ProviderError) || !error.retryable) {
|
|
81
|
+
this.states.delete(key);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (state?.probeInFlight) {
|
|
85
|
+
this.states.set(key, {
|
|
86
|
+
failures: this.failureThreshold,
|
|
87
|
+
openedAt: this.now(),
|
|
88
|
+
probeInFlight: false,
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const failures = (state?.failures ?? 0) + 1;
|
|
93
|
+
this.states.set(key, {
|
|
94
|
+
failures,
|
|
95
|
+
openedAt: failures >= this.failureThreshold ? this.now() : undefined,
|
|
96
|
+
probeInFlight: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
recordRequest(key) {
|
|
100
|
+
const observation = this.getObservation(key);
|
|
101
|
+
observation.totalRequests += 1;
|
|
102
|
+
}
|
|
103
|
+
recordSuccess(key) {
|
|
104
|
+
const observation = this.getObservation(key);
|
|
105
|
+
observation.totalSuccesses += 1;
|
|
106
|
+
observation.lastSuccessAt = this.now();
|
|
107
|
+
}
|
|
108
|
+
recordObservedFailure(key, error) {
|
|
109
|
+
const observation = this.getObservation(key);
|
|
110
|
+
observation.totalFailures += 1;
|
|
111
|
+
observation.lastFailureAt = this.now();
|
|
112
|
+
observation.lastFailureCode = error instanceof ProviderError ? error.code : "PROVIDER_ERROR";
|
|
113
|
+
if (!(error instanceof ProviderError)) {
|
|
114
|
+
observation.failureCounts.other += 1;
|
|
115
|
+
}
|
|
116
|
+
else if (error.code === "PROVIDER_TIMEOUT") {
|
|
117
|
+
observation.failureCounts.timeout += 1;
|
|
118
|
+
}
|
|
119
|
+
else if (error.code === "PROVIDER_RATE_LIMITED") {
|
|
120
|
+
observation.failureCounts.rateLimited += 1;
|
|
121
|
+
}
|
|
122
|
+
else if (error.code === "PROVIDER_UNAVAILABLE") {
|
|
123
|
+
observation.failureCounts.unavailable += 1;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
observation.failureCounts.other += 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
getObservation(key) {
|
|
130
|
+
const existing = this.observations.get(key);
|
|
131
|
+
if (existing) {
|
|
132
|
+
return existing;
|
|
133
|
+
}
|
|
134
|
+
const observation = {
|
|
135
|
+
totalRequests: 0,
|
|
136
|
+
totalSuccesses: 0,
|
|
137
|
+
totalFailures: 0,
|
|
138
|
+
failureCounts: createEmptyFailureCounts(),
|
|
139
|
+
};
|
|
140
|
+
this.observations.set(key, observation);
|
|
141
|
+
return observation;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function createEmptyFailureCounts() {
|
|
145
|
+
return {
|
|
146
|
+
timeout: 0,
|
|
147
|
+
rateLimited: 0,
|
|
148
|
+
unavailable: 0,
|
|
149
|
+
other: 0,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function createOpenCircuitError(provider) {
|
|
153
|
+
return new ProviderError({
|
|
154
|
+
provider,
|
|
155
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
156
|
+
message: `Provider "${provider}" circuit is open after repeated failures.`,
|
|
157
|
+
retryable: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function validatePositiveInteger(value, fallback, name) {
|
|
161
|
+
const normalized = value ?? fallback;
|
|
162
|
+
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
163
|
+
throw new TypeError(`Circuit breaker ${name} must be a positive integer.`);
|
|
164
|
+
}
|
|
165
|
+
return normalized;
|
|
166
|
+
}
|
|
167
|
+
function validateNonNegativeInteger(value, fallback, name) {
|
|
168
|
+
const normalized = value ?? fallback;
|
|
169
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
170
|
+
throw new TypeError(`Circuit breaker ${name} must be a non-negative integer.`);
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/engine/circuit-breaker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProviderConcurrencyOptions } from "./types.js";
|
|
2
|
+
export declare class ProviderConcurrencyLimiter {
|
|
3
|
+
private readonly defaultMaxConcurrent;
|
|
4
|
+
private readonly maxQueueSize;
|
|
5
|
+
private readonly providerLimits;
|
|
6
|
+
private readonly queues;
|
|
7
|
+
constructor(options?: ProviderConcurrencyOptions);
|
|
8
|
+
run<T>(key: string, provider: string, signal: AbortSignal | undefined, operation: () => Promise<T>): Promise<T>;
|
|
9
|
+
private getQueue;
|
|
10
|
+
private execute;
|
|
11
|
+
private drain;
|
|
12
|
+
private deleteIdleQueue;
|
|
13
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ProviderError } from "../errors/index.js";
|
|
2
|
+
const DEFAULT_MAX_CONCURRENT = 2;
|
|
3
|
+
const DEFAULT_MAX_QUEUE_SIZE = 100;
|
|
4
|
+
const MAX_CONCURRENT_BOUND = 100;
|
|
5
|
+
const MAX_QUEUE_SIZE_BOUND = 10_000;
|
|
6
|
+
// Bounds concurrent work independently for every metadata, streaming, or torrent provider.
|
|
7
|
+
// Ограничивает параллельную работу отдельно для metadata, streaming и torrent-провайдеров.
|
|
8
|
+
export class ProviderConcurrencyLimiter {
|
|
9
|
+
defaultMaxConcurrent;
|
|
10
|
+
maxQueueSize;
|
|
11
|
+
providerLimits;
|
|
12
|
+
queues = new Map();
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.defaultMaxConcurrent = validateBoundedInteger(options.defaultMaxConcurrent, DEFAULT_MAX_CONCURRENT, 1, MAX_CONCURRENT_BOUND, "defaultMaxConcurrent");
|
|
15
|
+
this.maxQueueSize = validateBoundedInteger(options.maxQueueSize, DEFAULT_MAX_QUEUE_SIZE, 0, MAX_QUEUE_SIZE_BOUND, "maxQueueSize");
|
|
16
|
+
this.providerLimits = new Map(Object.entries(options.providerLimits ?? {}).map(([provider, limit]) => [
|
|
17
|
+
validateProviderName(provider),
|
|
18
|
+
validateBoundedInteger(limit, this.defaultMaxConcurrent, 1, MAX_CONCURRENT_BOUND, `providerLimits.${provider}`),
|
|
19
|
+
]));
|
|
20
|
+
}
|
|
21
|
+
run(key, provider, signal, operation) {
|
|
22
|
+
if (signal?.aborted) {
|
|
23
|
+
return Promise.reject(signal.reason);
|
|
24
|
+
}
|
|
25
|
+
const queue = this.getQueue(key);
|
|
26
|
+
const limit = this.providerLimits.get(provider) ?? this.defaultMaxConcurrent;
|
|
27
|
+
if (queue.active < limit) {
|
|
28
|
+
return this.execute(key, queue, provider, operation);
|
|
29
|
+
}
|
|
30
|
+
if (queue.waiting.length >= this.maxQueueSize) {
|
|
31
|
+
return Promise.reject(createSaturatedError(provider));
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const entry = { provider, operation, resolve, reject, signal };
|
|
35
|
+
if (signal) {
|
|
36
|
+
entry.onAbort = () => {
|
|
37
|
+
const index = queue.waiting.indexOf(entry);
|
|
38
|
+
if (index !== -1) {
|
|
39
|
+
queue.waiting.splice(index, 1);
|
|
40
|
+
reject(signal.reason);
|
|
41
|
+
this.deleteIdleQueue(key, queue);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
signal.addEventListener("abort", entry.onAbort, { once: true });
|
|
45
|
+
}
|
|
46
|
+
queue.waiting.push(entry);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
getQueue(key) {
|
|
50
|
+
const existing = this.queues.get(key);
|
|
51
|
+
if (existing) {
|
|
52
|
+
return existing;
|
|
53
|
+
}
|
|
54
|
+
const queue = { active: 0, waiting: [] };
|
|
55
|
+
this.queues.set(key, queue);
|
|
56
|
+
return queue;
|
|
57
|
+
}
|
|
58
|
+
async execute(key, queue, provider, operation) {
|
|
59
|
+
queue.active += 1;
|
|
60
|
+
try {
|
|
61
|
+
return await operation();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
queue.active -= 1;
|
|
65
|
+
this.drain(key, queue, provider);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
drain(key, queue, provider) {
|
|
69
|
+
const limit = this.providerLimits.get(provider) ?? this.defaultMaxConcurrent;
|
|
70
|
+
while (queue.active < limit) {
|
|
71
|
+
const next = queue.waiting.shift();
|
|
72
|
+
if (!next) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (next.onAbort && next.signal) {
|
|
76
|
+
next.signal.removeEventListener("abort", next.onAbort);
|
|
77
|
+
}
|
|
78
|
+
void this.execute(key, queue, next.provider, next.operation).then(next.resolve, next.reject);
|
|
79
|
+
}
|
|
80
|
+
this.deleteIdleQueue(key, queue);
|
|
81
|
+
}
|
|
82
|
+
deleteIdleQueue(key, queue) {
|
|
83
|
+
if (queue.active === 0 && queue.waiting.length === 0) {
|
|
84
|
+
this.queues.delete(key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function createSaturatedError(provider) {
|
|
89
|
+
return new ProviderError({
|
|
90
|
+
provider,
|
|
91
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
92
|
+
message: `Provider "${provider}" concurrency queue is full.`,
|
|
93
|
+
retryable: true,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function validateProviderName(provider) {
|
|
97
|
+
if (!provider || provider.trim() !== provider) {
|
|
98
|
+
throw new TypeError("Provider concurrency override names must be non-empty and unpadded.");
|
|
99
|
+
}
|
|
100
|
+
return provider;
|
|
101
|
+
}
|
|
102
|
+
function validateBoundedInteger(value, fallback, minimum, maximum, name) {
|
|
103
|
+
const normalized = value ?? fallback;
|
|
104
|
+
if (!Number.isSafeInteger(normalized) || normalized < minimum || normalized > maximum) {
|
|
105
|
+
throw new TypeError(`Provider concurrency ${name} must be an integer between ${minimum} and ${maximum}.`);
|
|
106
|
+
}
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=concurrency-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency-limiter.js","sourceRoot":"","sources":["../../src/engine/concurrency-limiter.ts"],"names":[],"mappings":"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"}
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -4,26 +4,36 @@ import { type MergeStrategy } from "../merge/index.js";
|
|
|
4
4
|
import { ProviderRegistry, type ProviderInfo } from "../providers/index.js";
|
|
5
5
|
import type { SearchQuery, SearchResponse } from "../search/index.js";
|
|
6
6
|
import type { MediaAvailability, StreamQuery, StreamingProviderInfo } from "../streaming/index.js";
|
|
7
|
-
import type {
|
|
7
|
+
import type { TorrentDiscoveryQuery, TorrentDiscoveryResponse, TorrentProviderInfo } from "../torrent/index.js";
|
|
8
|
+
import type { MediaEngineOperationOptions, MediaEngineOptions, ProviderHealthStatus } from "./types.js";
|
|
8
9
|
export declare class MediaEngine {
|
|
9
10
|
private readonly registry;
|
|
10
11
|
private readonly streamingProviders;
|
|
12
|
+
private readonly torrentProviders;
|
|
11
13
|
private readonly cache?;
|
|
12
14
|
private readonly mergeStrategy;
|
|
13
15
|
private readonly timeoutMs?;
|
|
14
16
|
private readonly providerTimeouts;
|
|
15
17
|
private readonly debug;
|
|
18
|
+
private readonly circuitBreaker?;
|
|
19
|
+
private readonly concurrencyLimiter?;
|
|
20
|
+
private readonly inFlightRequests;
|
|
16
21
|
constructor(options?: MediaEngineOptions);
|
|
17
22
|
getProviders(): ProviderInfo[];
|
|
18
23
|
getStreamingProviders(): StreamingProviderInfo[];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
getTorrentProviders(): TorrentProviderInfo[];
|
|
25
|
+
getProviderHealth(): ProviderHealthStatus[];
|
|
26
|
+
search(query: SearchQuery, options?: MediaEngineOperationOptions): Promise<SearchResponse>;
|
|
27
|
+
getDetails(query: DetailsQuery, options?: MediaEngineOperationOptions): Promise<DetailsResponse>;
|
|
28
|
+
getAvailability(query: StreamQuery, options?: MediaEngineOperationOptions): Promise<MediaAvailability>;
|
|
29
|
+
discoverTorrents(query: TorrentDiscoveryQuery, options?: MediaEngineOperationOptions): Promise<TorrentDiscoveryResponse>;
|
|
22
30
|
protected get providerRegistry(): ProviderRegistry;
|
|
23
31
|
protected get engineCache(): Cache | undefined;
|
|
24
32
|
protected get engineMergeStrategy(): MergeStrategy;
|
|
25
33
|
protected get engineTimeoutMs(): number | undefined;
|
|
26
34
|
private getProviderTimeoutMs;
|
|
27
|
-
private
|
|
35
|
+
private createProviderTimeoutBudget;
|
|
36
|
+
private loadReusableDetails;
|
|
37
|
+
private createProviderHealthStatus;
|
|
28
38
|
protected get engineDebug(): boolean;
|
|
29
39
|
}
|