@livepeer-frameworks/player-core 0.0.3
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/dist/cjs/index.js +19493 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +19398 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/player.css +2140 -0
- package/dist/types/core/ABRController.d.ts +164 -0
- package/dist/types/core/CodecUtils.d.ts +54 -0
- package/dist/types/core/Disposable.d.ts +61 -0
- package/dist/types/core/EventEmitter.d.ts +73 -0
- package/dist/types/core/GatewayClient.d.ts +144 -0
- package/dist/types/core/InteractionController.d.ts +121 -0
- package/dist/types/core/LiveDurationProxy.d.ts +102 -0
- package/dist/types/core/MetaTrackManager.d.ts +220 -0
- package/dist/types/core/MistReporter.d.ts +163 -0
- package/dist/types/core/MistSignaling.d.ts +148 -0
- package/dist/types/core/PlayerController.d.ts +665 -0
- package/dist/types/core/PlayerInterface.d.ts +230 -0
- package/dist/types/core/PlayerManager.d.ts +182 -0
- package/dist/types/core/PlayerRegistry.d.ts +27 -0
- package/dist/types/core/QualityMonitor.d.ts +184 -0
- package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
- package/dist/types/core/SeekingUtils.d.ts +142 -0
- package/dist/types/core/StreamStateClient.d.ts +108 -0
- package/dist/types/core/SubtitleManager.d.ts +111 -0
- package/dist/types/core/TelemetryReporter.d.ts +79 -0
- package/dist/types/core/TimeFormat.d.ts +97 -0
- package/dist/types/core/TimerManager.d.ts +83 -0
- package/dist/types/core/UrlUtils.d.ts +81 -0
- package/dist/types/core/detector.d.ts +149 -0
- package/dist/types/core/index.d.ts +49 -0
- package/dist/types/core/scorer.d.ts +167 -0
- package/dist/types/core/selector.d.ts +9 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/players/DashJsPlayer.d.ts +102 -0
- package/dist/types/players/HlsJsPlayer.d.ts +70 -0
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
- package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
- package/dist/types/players/MistPlayer.d.ts +25 -0
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
- package/dist/types/players/NativePlayer.d.ts +143 -0
- package/dist/types/players/VideoJsPlayer.d.ts +59 -0
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
- package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
- package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
- package/dist/types/players/index.d.ts +14 -0
- package/dist/types/styles/index.d.ts +11 -0
- package/dist/types/types.d.ts +363 -0
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
- package/dist/types/vanilla/index.d.ts +19 -0
- package/dist/workers/decoder.worker.js +989 -0
- package/dist/workers/decoder.worker.js.map +1 -0
- package/package.json +80 -0
- package/src/core/ABRController.ts +550 -0
- package/src/core/CodecUtils.ts +257 -0
- package/src/core/Disposable.ts +120 -0
- package/src/core/EventEmitter.ts +113 -0
- package/src/core/GatewayClient.ts +439 -0
- package/src/core/InteractionController.ts +712 -0
- package/src/core/LiveDurationProxy.ts +270 -0
- package/src/core/MetaTrackManager.ts +753 -0
- package/src/core/MistReporter.ts +543 -0
- package/src/core/MistSignaling.ts +346 -0
- package/src/core/PlayerController.ts +2829 -0
- package/src/core/PlayerInterface.ts +432 -0
- package/src/core/PlayerManager.ts +900 -0
- package/src/core/PlayerRegistry.ts +149 -0
- package/src/core/QualityMonitor.ts +597 -0
- package/src/core/ScreenWakeLockManager.ts +163 -0
- package/src/core/SeekingUtils.ts +364 -0
- package/src/core/StreamStateClient.ts +457 -0
- package/src/core/SubtitleManager.ts +297 -0
- package/src/core/TelemetryReporter.ts +308 -0
- package/src/core/TimeFormat.ts +205 -0
- package/src/core/TimerManager.ts +209 -0
- package/src/core/UrlUtils.ts +179 -0
- package/src/core/detector.ts +382 -0
- package/src/core/index.ts +140 -0
- package/src/core/scorer.ts +553 -0
- package/src/core/selector.ts +16 -0
- package/src/global.d.ts +11 -0
- package/src/index.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/players/DashJsPlayer.ts +642 -0
- package/src/players/HlsJsPlayer.ts +483 -0
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
- package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
- package/src/players/MewsWsPlayer/index.ts +1065 -0
- package/src/players/MewsWsPlayer/types.ts +106 -0
- package/src/players/MistPlayer.ts +188 -0
- package/src/players/MistWebRTCPlayer/index.ts +703 -0
- package/src/players/NativePlayer.ts +820 -0
- package/src/players/VideoJsPlayer.ts +643 -0
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
- package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
- package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
- package/src/players/WebCodecsPlayer/index.ts +1650 -0
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
- package/src/players/WebCodecsPlayer/types.ts +542 -0
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
- package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
- package/src/players/index.ts +22 -0
- package/src/styles/animations.css +21 -0
- package/src/styles/index.ts +52 -0
- package/src/styles/player.css +2126 -0
- package/src/styles/tailwind.css +1015 -0
- package/src/types.ts +421 -0
- package/src/vanilla/FrameWorksPlayer.ts +367 -0
- package/src/vanilla/index.ts +22 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GatewayClient.ts
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic client for resolving viewer endpoints from the Gateway GraphQL API.
|
|
5
|
+
* Extracted from useViewerEndpoints.ts for use in headless core.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { TypedEventEmitter } from './EventEmitter';
|
|
9
|
+
import type { ContentEndpoints, ContentType } from '../types';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export type GatewayStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
16
|
+
|
|
17
|
+
export interface GatewayClientConfig {
|
|
18
|
+
/** Gateway GraphQL endpoint URL */
|
|
19
|
+
gatewayUrl: string;
|
|
20
|
+
/** Content type to resolve */
|
|
21
|
+
contentType: ContentType;
|
|
22
|
+
/** Content identifier (stream name) */
|
|
23
|
+
contentId: string;
|
|
24
|
+
/** Optional auth token for private streams */
|
|
25
|
+
authToken?: string;
|
|
26
|
+
/** Maximum retry attempts (default: 3) */
|
|
27
|
+
maxRetries?: number;
|
|
28
|
+
/** Initial retry delay in ms (default: 500) */
|
|
29
|
+
initialDelayMs?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GatewayClientEvents {
|
|
33
|
+
/** Emitted when status changes */
|
|
34
|
+
statusChange: { status: GatewayStatus; error?: string };
|
|
35
|
+
/** Emitted when endpoints are successfully resolved */
|
|
36
|
+
endpointsResolved: { endpoints: ContentEndpoints };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Constants
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
44
|
+
const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
45
|
+
// F2: Cache TTL for resolved endpoints
|
|
46
|
+
const DEFAULT_CACHE_TTL_MS = 10000;
|
|
47
|
+
// F3: Circuit breaker constants
|
|
48
|
+
const CIRCUIT_BREAKER_THRESHOLD = 5; // Open after 5 consecutive failures
|
|
49
|
+
const CIRCUIT_BREAKER_TIMEOUT_MS = 30000; // Half-open after 30 seconds
|
|
50
|
+
|
|
51
|
+
type CircuitBreakerState = 'closed' | 'open' | 'half-open';
|
|
52
|
+
|
|
53
|
+
const RESOLVE_VIEWER_QUERY = `
|
|
54
|
+
query ResolveViewer($contentType: String!, $contentId: String!) {
|
|
55
|
+
resolveViewerEndpoint(contentType: $contentType, contentId: $contentId) {
|
|
56
|
+
primary { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
57
|
+
fallbacks { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
58
|
+
metadata { contentType contentId title description durationSeconds status isLive viewers recordingSizeBytes clipSource createdAt }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Helper Functions
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch with exponential backoff retry logic.
|
|
69
|
+
*/
|
|
70
|
+
async function fetchWithRetry(
|
|
71
|
+
url: string,
|
|
72
|
+
options: RequestInit,
|
|
73
|
+
maxRetries: number,
|
|
74
|
+
initialDelay: number
|
|
75
|
+
): Promise<Response> {
|
|
76
|
+
let lastError: Error | null = null;
|
|
77
|
+
|
|
78
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(url, options);
|
|
81
|
+
return response;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
lastError = e instanceof Error ? e : new Error('Fetch failed');
|
|
84
|
+
|
|
85
|
+
// Don't retry on abort
|
|
86
|
+
if (options.signal?.aborted) {
|
|
87
|
+
throw lastError;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Wait before retrying (exponential backoff)
|
|
91
|
+
if (attempt < maxRetries - 1) {
|
|
92
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
93
|
+
console.warn(`[GatewayClient] Retry ${attempt + 1}/${maxRetries - 1} after ${delay}ms`);
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw lastError ?? new Error('Gateway unreachable after retries');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// GatewayClient Class
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Client for resolving viewer endpoints from the Gateway GraphQL API.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const client = new GatewayClient({
|
|
112
|
+
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
113
|
+
* contentType: 'live',
|
|
114
|
+
* contentId: 'my-stream',
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* client.on('statusChange', ({ status }) => console.log('Status:', status));
|
|
118
|
+
* client.on('endpointsResolved', ({ endpoints }) => console.log('Endpoints:', endpoints));
|
|
119
|
+
*
|
|
120
|
+
* const endpoints = await client.resolve();
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
124
|
+
private config: GatewayClientConfig;
|
|
125
|
+
private status: GatewayStatus = 'idle';
|
|
126
|
+
private endpoints: ContentEndpoints | null = null;
|
|
127
|
+
private error: string | null = null;
|
|
128
|
+
private abortController: AbortController | null = null;
|
|
129
|
+
|
|
130
|
+
// F2: Request deduplication - in-flight request tracking
|
|
131
|
+
private inFlightRequest: Promise<ContentEndpoints> | null = null;
|
|
132
|
+
// F2: Cache with TTL for resolved endpoints
|
|
133
|
+
private cacheTimestamp = 0;
|
|
134
|
+
private cacheTtlMs: number;
|
|
135
|
+
|
|
136
|
+
// F3: Circuit breaker state
|
|
137
|
+
private circuitState: CircuitBreakerState = 'closed';
|
|
138
|
+
private consecutiveFailures = 0;
|
|
139
|
+
private circuitOpenedAt = 0;
|
|
140
|
+
|
|
141
|
+
constructor(config: GatewayClientConfig) {
|
|
142
|
+
super();
|
|
143
|
+
this.config = config;
|
|
144
|
+
this.cacheTtlMs = DEFAULT_CACHE_TTL_MS;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve endpoints from the gateway.
|
|
149
|
+
* F2: Returns cached result if still valid, deduplicates concurrent requests.
|
|
150
|
+
* F3: Respects circuit breaker state.
|
|
151
|
+
* @param forceRefresh - If true, bypasses cache and fetches fresh data
|
|
152
|
+
* @returns Promise resolving to ContentEndpoints
|
|
153
|
+
* @throws Error if resolution fails after retries or circuit is open
|
|
154
|
+
*/
|
|
155
|
+
async resolve(forceRefresh = false): Promise<ContentEndpoints> {
|
|
156
|
+
// F2: Return cached result if still valid
|
|
157
|
+
if (!forceRefresh && this.endpoints && this.isCacheValid()) {
|
|
158
|
+
return this.endpoints;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// F3: Check circuit breaker
|
|
162
|
+
if (!this.canAttemptRequest()) {
|
|
163
|
+
throw new Error('Circuit breaker is open - too many recent failures');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// F2: Return in-flight request if one exists (deduplication)
|
|
167
|
+
if (this.inFlightRequest) {
|
|
168
|
+
return this.inFlightRequest;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Create a new request and track it
|
|
172
|
+
this.inFlightRequest = this.doResolve();
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const result = await this.inFlightRequest;
|
|
176
|
+
// F3: Success - close circuit
|
|
177
|
+
this.onSuccess();
|
|
178
|
+
return result;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// F3: Failure - record for circuit breaker
|
|
181
|
+
this.onFailure();
|
|
182
|
+
throw e;
|
|
183
|
+
} finally {
|
|
184
|
+
this.inFlightRequest = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* F2: Check if cache is still valid
|
|
190
|
+
*/
|
|
191
|
+
private isCacheValid(): boolean {
|
|
192
|
+
return Date.now() - this.cacheTimestamp < this.cacheTtlMs;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* F2: Set cache TTL (for testing or custom requirements)
|
|
197
|
+
*/
|
|
198
|
+
setCacheTtl(ttlMs: number): void {
|
|
199
|
+
this.cacheTtlMs = ttlMs;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* F2: Invalidate the cache manually
|
|
204
|
+
*/
|
|
205
|
+
invalidateCache(): void {
|
|
206
|
+
this.cacheTimestamp = 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ==========================================================================
|
|
210
|
+
// F3: Circuit Breaker Methods
|
|
211
|
+
// ==========================================================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* F3: Check if a request can be attempted based on circuit state
|
|
215
|
+
*/
|
|
216
|
+
private canAttemptRequest(): boolean {
|
|
217
|
+
switch (this.circuitState) {
|
|
218
|
+
case 'closed':
|
|
219
|
+
return true;
|
|
220
|
+
|
|
221
|
+
case 'open':
|
|
222
|
+
// Check if enough time has passed to try half-open
|
|
223
|
+
if (Date.now() - this.circuitOpenedAt >= CIRCUIT_BREAKER_TIMEOUT_MS) {
|
|
224
|
+
this.circuitState = 'half-open';
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
|
|
229
|
+
case 'half-open':
|
|
230
|
+
// Allow one request to test the circuit
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* F3: Record a successful request
|
|
237
|
+
*/
|
|
238
|
+
private onSuccess(): void {
|
|
239
|
+
this.consecutiveFailures = 0;
|
|
240
|
+
this.circuitState = 'closed';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* F3: Record a failed request
|
|
245
|
+
*/
|
|
246
|
+
private onFailure(): void {
|
|
247
|
+
this.consecutiveFailures++;
|
|
248
|
+
|
|
249
|
+
if (this.circuitState === 'half-open') {
|
|
250
|
+
// Failed during half-open - re-open the circuit
|
|
251
|
+
this.circuitState = 'open';
|
|
252
|
+
this.circuitOpenedAt = Date.now();
|
|
253
|
+
} else if (this.consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
254
|
+
// Threshold reached - open the circuit
|
|
255
|
+
this.circuitState = 'open';
|
|
256
|
+
this.circuitOpenedAt = Date.now();
|
|
257
|
+
console.warn(`[GatewayClient] Circuit breaker opened after ${this.consecutiveFailures} consecutive failures`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* F3: Get current circuit breaker state (for monitoring/debugging)
|
|
263
|
+
*/
|
|
264
|
+
getCircuitState(): { state: CircuitBreakerState; failures: number; openedAt: number | null } {
|
|
265
|
+
return {
|
|
266
|
+
state: this.circuitState,
|
|
267
|
+
failures: this.consecutiveFailures,
|
|
268
|
+
openedAt: this.circuitState === 'open' ? this.circuitOpenedAt : null,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* F3: Manually reset the circuit breaker
|
|
274
|
+
*/
|
|
275
|
+
resetCircuitBreaker(): void {
|
|
276
|
+
this.circuitState = 'closed';
|
|
277
|
+
this.consecutiveFailures = 0;
|
|
278
|
+
this.circuitOpenedAt = 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Internal method to perform the actual resolution.
|
|
283
|
+
* @returns Promise resolving to ContentEndpoints
|
|
284
|
+
*/
|
|
285
|
+
private async doResolve(): Promise<ContentEndpoints> {
|
|
286
|
+
// Abort any in-flight fetch (different from inFlightRequest promise tracking)
|
|
287
|
+
this.abort();
|
|
288
|
+
|
|
289
|
+
const {
|
|
290
|
+
gatewayUrl,
|
|
291
|
+
contentType,
|
|
292
|
+
contentId,
|
|
293
|
+
authToken,
|
|
294
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
295
|
+
initialDelayMs = DEFAULT_INITIAL_DELAY_MS,
|
|
296
|
+
} = this.config;
|
|
297
|
+
|
|
298
|
+
// Validate required params
|
|
299
|
+
if (!gatewayUrl || !contentType || !contentId) {
|
|
300
|
+
const error = 'Missing required parameters: gatewayUrl, contentType, or contentId';
|
|
301
|
+
this.setStatus('error', error);
|
|
302
|
+
throw new Error(error);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.setStatus('loading');
|
|
306
|
+
|
|
307
|
+
const ac = new AbortController();
|
|
308
|
+
this.abortController = ac;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const graphqlEndpoint = gatewayUrl.replace(/\/$/, '');
|
|
312
|
+
|
|
313
|
+
const res = await fetchWithRetry(
|
|
314
|
+
graphqlEndpoint,
|
|
315
|
+
{
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers: {
|
|
318
|
+
'Content-Type': 'application/json',
|
|
319
|
+
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
320
|
+
},
|
|
321
|
+
body: JSON.stringify({
|
|
322
|
+
query: RESOLVE_VIEWER_QUERY,
|
|
323
|
+
variables: { contentType, contentId },
|
|
324
|
+
}),
|
|
325
|
+
signal: ac.signal,
|
|
326
|
+
},
|
|
327
|
+
maxRetries,
|
|
328
|
+
initialDelayMs
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (!res.ok) {
|
|
332
|
+
throw new Error(`Gateway GQL error ${res.status}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const payload = await res.json();
|
|
336
|
+
|
|
337
|
+
if (payload.errors?.length) {
|
|
338
|
+
throw new Error(payload.errors[0]?.message || 'GraphQL error');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const resp = payload.data?.resolveViewerEndpoint;
|
|
342
|
+
const primary = resp?.primary;
|
|
343
|
+
const fallbacks = Array.isArray(resp?.fallbacks) ? resp.fallbacks : [];
|
|
344
|
+
|
|
345
|
+
if (!primary) {
|
|
346
|
+
throw new Error('No endpoints available');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const endpoints: ContentEndpoints = {
|
|
350
|
+
primary,
|
|
351
|
+
fallbacks,
|
|
352
|
+
metadata: resp?.metadata,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
this.endpoints = endpoints;
|
|
356
|
+
// F2: Update cache timestamp
|
|
357
|
+
this.cacheTimestamp = Date.now();
|
|
358
|
+
this.setStatus('ready');
|
|
359
|
+
this.emit('endpointsResolved', { endpoints });
|
|
360
|
+
|
|
361
|
+
return endpoints;
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Ignore abort errors
|
|
364
|
+
if (ac.signal.aborted) {
|
|
365
|
+
throw new Error('Request aborted');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const message = e instanceof Error ? e.message : 'Unknown gateway error';
|
|
369
|
+
console.error('[GatewayClient] Gateway resolution failed:', message);
|
|
370
|
+
this.setStatus('error', message);
|
|
371
|
+
throw new Error(message);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Abort any in-flight request.
|
|
377
|
+
*/
|
|
378
|
+
abort(): void {
|
|
379
|
+
if (this.abortController) {
|
|
380
|
+
this.abortController.abort();
|
|
381
|
+
this.abortController = null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get current status.
|
|
387
|
+
*/
|
|
388
|
+
getStatus(): GatewayStatus {
|
|
389
|
+
return this.status;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get resolved endpoints (null if not yet resolved).
|
|
394
|
+
*/
|
|
395
|
+
getEndpoints(): ContentEndpoints | null {
|
|
396
|
+
return this.endpoints;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get error message (null if no error).
|
|
401
|
+
*/
|
|
402
|
+
getError(): string | null {
|
|
403
|
+
return this.error;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Update configuration and reset state.
|
|
408
|
+
* F2: Also clears cache and in-flight request
|
|
409
|
+
* F3: Resets circuit breaker (new config = fresh start)
|
|
410
|
+
*/
|
|
411
|
+
updateConfig(config: Partial<GatewayClientConfig>): void {
|
|
412
|
+
this.abort();
|
|
413
|
+
this.config = { ...this.config, ...config };
|
|
414
|
+
this.endpoints = null;
|
|
415
|
+
this.error = null;
|
|
416
|
+
// F2: Clear cache and in-flight tracking
|
|
417
|
+
this.cacheTimestamp = 0;
|
|
418
|
+
this.inFlightRequest = null;
|
|
419
|
+
// F3: Reset circuit breaker for new config
|
|
420
|
+
this.resetCircuitBreaker();
|
|
421
|
+
this.setStatus('idle');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Clean up resources.
|
|
426
|
+
*/
|
|
427
|
+
destroy(): void {
|
|
428
|
+
this.abort();
|
|
429
|
+
this.removeAllListeners();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private setStatus(status: GatewayStatus, error?: string): void {
|
|
433
|
+
this.status = status;
|
|
434
|
+
this.error = error ?? null;
|
|
435
|
+
this.emit('statusChange', { status, error });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export default GatewayClient;
|