@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,36 @@
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
+ getSnapshot(key: string): CircuitBreakerSnapshot;
29
+ private acquire;
30
+ private recordFailure;
31
+ private recordRequest;
32
+ private recordSuccess;
33
+ private recordObservedFailure;
34
+ private getObservation;
35
+ }
36
+ export {};
@@ -0,0 +1,163 @@
1
+ import { ProviderError } from "../errors/index.js";
2
+ const DEFAULT_FAILURE_THRESHOLD = 3;
3
+ const DEFAULT_RECOVERY_TIMEOUT_MS = 30_000;
4
+ // Tracks transient provider failures and allows one recovery probe after cooldown.
5
+ // Отслеживает временные сбои провайдера и разрешает одну пробу после cooldown.
6
+ export class ProviderCircuitBreaker {
7
+ failureThreshold;
8
+ recoveryTimeoutMs;
9
+ now;
10
+ states = new Map();
11
+ observations = new Map();
12
+ constructor(options = {}, now = Date.now) {
13
+ this.failureThreshold = validatePositiveInteger(options.failureThreshold, DEFAULT_FAILURE_THRESHOLD, "failureThreshold");
14
+ this.recoveryTimeoutMs = validateNonNegativeInteger(options.recoveryTimeoutMs, DEFAULT_RECOVERY_TIMEOUT_MS, "recoveryTimeoutMs");
15
+ this.now = now;
16
+ }
17
+ async run(key, provider, operation) {
18
+ this.acquire(key, provider);
19
+ this.recordRequest(key);
20
+ try {
21
+ const result = await operation();
22
+ this.states.delete(key);
23
+ this.recordSuccess(key);
24
+ return result;
25
+ }
26
+ catch (error) {
27
+ this.recordFailure(key, error);
28
+ this.recordObservedFailure(key, error);
29
+ throw error;
30
+ }
31
+ }
32
+ getSnapshot(key) {
33
+ const now = this.now();
34
+ const state = this.states.get(key);
35
+ const observation = this.observations.get(key);
36
+ const circuitState = state?.probeInFlight
37
+ ? "half-open"
38
+ : state?.openedAt !== undefined
39
+ ? "open"
40
+ : "closed";
41
+ const retryAfterMs = state?.openedAt === undefined
42
+ ? undefined
43
+ : Math.max(0, this.recoveryTimeoutMs - (now - state.openedAt));
44
+ return {
45
+ state: circuitState,
46
+ consecutiveFailures: state?.failures ?? 0,
47
+ totalRequests: observation?.totalRequests ?? 0,
48
+ totalSuccesses: observation?.totalSuccesses ?? 0,
49
+ totalFailures: observation?.totalFailures ?? 0,
50
+ lastSuccessAt: observation?.lastSuccessAt,
51
+ lastFailureAt: observation?.lastFailureAt,
52
+ lastFailureCode: observation?.lastFailureCode,
53
+ failureCounts: observation?.failureCounts ?? createEmptyFailureCounts(),
54
+ retryAfterMs,
55
+ };
56
+ }
57
+ acquire(key, provider) {
58
+ const state = this.states.get(key);
59
+ if (state?.openedAt === undefined) {
60
+ return;
61
+ }
62
+ if (this.now() - state.openedAt < this.recoveryTimeoutMs || state.probeInFlight) {
63
+ throw createOpenCircuitError(provider);
64
+ }
65
+ state.probeInFlight = true;
66
+ }
67
+ recordFailure(key, error) {
68
+ const state = this.states.get(key);
69
+ if (!(error instanceof ProviderError) || !error.retryable) {
70
+ this.states.delete(key);
71
+ return;
72
+ }
73
+ if (state?.probeInFlight) {
74
+ this.states.set(key, {
75
+ failures: this.failureThreshold,
76
+ openedAt: this.now(),
77
+ probeInFlight: false,
78
+ });
79
+ return;
80
+ }
81
+ const failures = (state?.failures ?? 0) + 1;
82
+ this.states.set(key, {
83
+ failures,
84
+ openedAt: failures >= this.failureThreshold ? this.now() : undefined,
85
+ probeInFlight: false,
86
+ });
87
+ }
88
+ recordRequest(key) {
89
+ const observation = this.getObservation(key);
90
+ observation.totalRequests += 1;
91
+ }
92
+ recordSuccess(key) {
93
+ const observation = this.getObservation(key);
94
+ observation.totalSuccesses += 1;
95
+ observation.lastSuccessAt = this.now();
96
+ }
97
+ recordObservedFailure(key, error) {
98
+ const observation = this.getObservation(key);
99
+ observation.totalFailures += 1;
100
+ observation.lastFailureAt = this.now();
101
+ observation.lastFailureCode = error instanceof ProviderError ? error.code : "PROVIDER_ERROR";
102
+ if (!(error instanceof ProviderError)) {
103
+ observation.failureCounts.other += 1;
104
+ }
105
+ else if (error.code === "PROVIDER_TIMEOUT") {
106
+ observation.failureCounts.timeout += 1;
107
+ }
108
+ else if (error.code === "PROVIDER_RATE_LIMITED") {
109
+ observation.failureCounts.rateLimited += 1;
110
+ }
111
+ else if (error.code === "PROVIDER_UNAVAILABLE") {
112
+ observation.failureCounts.unavailable += 1;
113
+ }
114
+ else {
115
+ observation.failureCounts.other += 1;
116
+ }
117
+ }
118
+ getObservation(key) {
119
+ const existing = this.observations.get(key);
120
+ if (existing) {
121
+ return existing;
122
+ }
123
+ const observation = {
124
+ totalRequests: 0,
125
+ totalSuccesses: 0,
126
+ totalFailures: 0,
127
+ failureCounts: createEmptyFailureCounts(),
128
+ };
129
+ this.observations.set(key, observation);
130
+ return observation;
131
+ }
132
+ }
133
+ function createEmptyFailureCounts() {
134
+ return {
135
+ timeout: 0,
136
+ rateLimited: 0,
137
+ unavailable: 0,
138
+ other: 0,
139
+ };
140
+ }
141
+ function createOpenCircuitError(provider) {
142
+ return new ProviderError({
143
+ provider,
144
+ code: "PROVIDER_UNAVAILABLE",
145
+ message: `Provider "${provider}" circuit is open after repeated failures.`,
146
+ retryable: true,
147
+ });
148
+ }
149
+ function validatePositiveInteger(value, fallback, name) {
150
+ const normalized = value ?? fallback;
151
+ if (!Number.isSafeInteger(normalized) || normalized <= 0) {
152
+ throw new TypeError(`Circuit breaker ${name} must be a positive integer.`);
153
+ }
154
+ return normalized;
155
+ }
156
+ function validateNonNegativeInteger(value, fallback, name) {
157
+ const normalized = value ?? fallback;
158
+ if (!Number.isSafeInteger(normalized) || normalized < 0) {
159
+ throw new TypeError(`Circuit breaker ${name} must be a non-negative integer.`);
160
+ }
161
+ return normalized;
162
+ }
163
+ //# 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 or streaming provider.
7
+ // Ограничивает параллельную работу отдельно для каждого metadata/streaming-провайдера.
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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"}
@@ -4,7 +4,7 @@ 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 { MediaEngineOptions } from "./types.js";
7
+ import type { MediaEngineOptions, ProviderHealthStatus } from "./types.js";
8
8
  export declare class MediaEngine {
9
9
  private readonly registry;
10
10
  private readonly streamingProviders;
@@ -13,9 +13,13 @@ export declare class MediaEngine {
13
13
  private readonly timeoutMs?;
14
14
  private readonly providerTimeouts;
15
15
  private readonly debug;
16
+ private readonly circuitBreaker?;
17
+ private readonly concurrencyLimiter?;
18
+ private readonly inFlightRequests;
16
19
  constructor(options?: MediaEngineOptions);
17
20
  getProviders(): ProviderInfo[];
18
21
  getStreamingProviders(): StreamingProviderInfo[];
22
+ getProviderHealth(): ProviderHealthStatus[];
19
23
  search(query: SearchQuery): Promise<SearchResponse>;
20
24
  getDetails(query: DetailsQuery): Promise<DetailsResponse>;
21
25
  getAvailability(query: StreamQuery): Promise<MediaAvailability>;
@@ -24,6 +28,7 @@ export declare class MediaEngine {
24
28
  protected get engineMergeStrategy(): MergeStrategy;
25
29
  protected get engineTimeoutMs(): number | undefined;
26
30
  private getProviderTimeoutMs;
27
- private loadSearchPoster;
31
+ private createProviderTimeoutBudget;
32
+ private createProviderHealthStatus;
28
33
  protected get engineDebug(): boolean;
29
34
  }