@livepeer-frameworks/player-core 0.0.4 → 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.
- package/README.md +21 -6
- package/dist/cjs/index.js +792 -146
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +792 -146
- package/dist/esm/index.js.map +1 -1
- package/dist/player.css +185 -373
- package/dist/types/core/GatewayClient.d.ts +3 -4
- package/dist/types/core/InteractionController.d.ts +12 -0
- package/dist/types/core/MetaTrackManager.d.ts +1 -1
- package/dist/types/core/PlayerController.d.ts +18 -2
- package/dist/types/core/PlayerInterface.d.ts +10 -0
- package/dist/types/core/SeekingUtils.d.ts +3 -1
- package/dist/types/core/StreamStateClient.d.ts +1 -1
- package/dist/types/players/HlsJsPlayer.d.ts +8 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +1 -1
- package/dist/types/players/VideoJsPlayer.d.ts +12 -4
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/index.d.ts +11 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +25 -3
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +20 -2
- package/dist/types/types.d.ts +32 -1
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +5 -5
- package/dist/types/vanilla/index.d.ts +3 -3
- package/dist/workers/decoder.worker.js +183 -6
- package/dist/workers/decoder.worker.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ABRController.ts +38 -36
- package/src/core/CodecUtils.ts +50 -47
- package/src/core/Disposable.ts +4 -4
- package/src/core/EventEmitter.ts +1 -1
- package/src/core/GatewayClient.ts +48 -48
- package/src/core/InteractionController.ts +89 -82
- package/src/core/LiveDurationProxy.ts +14 -16
- package/src/core/MetaTrackManager.ts +74 -66
- package/src/core/MistReporter.ts +72 -45
- package/src/core/MistSignaling.ts +59 -56
- package/src/core/PlayerController.ts +724 -375
- package/src/core/PlayerInterface.ts +89 -59
- package/src/core/PlayerManager.ts +118 -123
- package/src/core/PlayerRegistry.ts +59 -42
- package/src/core/QualityMonitor.ts +38 -31
- package/src/core/ScreenWakeLockManager.ts +8 -9
- package/src/core/SeekingUtils.ts +31 -22
- package/src/core/StreamStateClient.ts +75 -69
- package/src/core/SubtitleManager.ts +25 -23
- package/src/core/TelemetryReporter.ts +34 -31
- package/src/core/TimeFormat.ts +13 -17
- package/src/core/TimerManager.ts +25 -9
- package/src/core/UrlUtils.ts +20 -17
- package/src/core/detector.ts +44 -44
- package/src/core/index.ts +57 -48
- package/src/core/scorer.ts +137 -138
- package/src/core/selector.ts +2 -6
- package/src/global.d.ts +1 -1
- package/src/index.ts +46 -35
- package/src/players/DashJsPlayer.ts +175 -114
- package/src/players/HlsJsPlayer.ts +154 -76
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +44 -39
- package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -10
- package/src/players/MewsWsPlayer/index.ts +196 -154
- package/src/players/MewsWsPlayer/types.ts +21 -21
- package/src/players/MistPlayer.ts +46 -27
- package/src/players/MistWebRTCPlayer/index.ts +175 -129
- package/src/players/NativePlayer.ts +203 -143
- package/src/players/VideoJsPlayer.ts +200 -146
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +6 -7
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +43 -43
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +10 -10
- package/src/players/WebCodecsPlayer/SyncController.ts +46 -55
- package/src/players/WebCodecsPlayer/WebSocketController.ts +67 -69
- package/src/players/WebCodecsPlayer/index.ts +280 -220
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
- package/src/players/WebCodecsPlayer/types.ts +81 -53
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +255 -192
- package/src/players/WebCodecsPlayer/worker/types.ts +33 -29
- package/src/players/index.ts +8 -8
- package/src/styles/animations.css +2 -1
- package/src/styles/player.css +182 -356
- package/src/styles/tailwind.css +473 -159
- package/src/types.ts +75 -33
- package/src/vanilla/FrameWorksPlayer.ts +34 -19
- package/src/vanilla/index.ts +7 -7
package/src/core/CodecUtils.ts
CHANGED
|
@@ -40,61 +40,61 @@ export function translateCodec(track: TrackInfo): string {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Audio codecs
|
|
43
|
-
if (track.type ===
|
|
43
|
+
if (track.type === "audio") {
|
|
44
44
|
switch (codec) {
|
|
45
|
-
case
|
|
46
|
-
case
|
|
47
|
-
return
|
|
48
|
-
case
|
|
49
|
-
return
|
|
50
|
-
case
|
|
51
|
-
case
|
|
52
|
-
return
|
|
53
|
-
case
|
|
54
|
-
case
|
|
55
|
-
case
|
|
56
|
-
case
|
|
57
|
-
return
|
|
58
|
-
case
|
|
59
|
-
return
|
|
60
|
-
case
|
|
61
|
-
return
|
|
62
|
-
case
|
|
63
|
-
return
|
|
64
|
-
case
|
|
65
|
-
case
|
|
66
|
-
return
|
|
45
|
+
case "AAC":
|
|
46
|
+
case "MP4A":
|
|
47
|
+
return "mp4a.40.2"; // AAC-LC
|
|
48
|
+
case "MP3":
|
|
49
|
+
return "mp4a.40.34"; // MP3 in MP4 container
|
|
50
|
+
case "AC3":
|
|
51
|
+
case "AC-3":
|
|
52
|
+
return "ac-3";
|
|
53
|
+
case "EAC3":
|
|
54
|
+
case "EC3":
|
|
55
|
+
case "E-AC3":
|
|
56
|
+
case "EC-3":
|
|
57
|
+
return "ec-3";
|
|
58
|
+
case "OPUS":
|
|
59
|
+
return "opus";
|
|
60
|
+
case "VORBIS":
|
|
61
|
+
return "vorbis";
|
|
62
|
+
case "FLAC":
|
|
63
|
+
return "flac";
|
|
64
|
+
case "PCM":
|
|
65
|
+
case "PCMS16LE":
|
|
66
|
+
return "pcm";
|
|
67
67
|
default:
|
|
68
68
|
return codec.toLowerCase();
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Video codecs
|
|
73
|
-
if (track.type ===
|
|
73
|
+
if (track.type === "video") {
|
|
74
74
|
switch (codec) {
|
|
75
|
-
case
|
|
76
|
-
case
|
|
77
|
-
case
|
|
75
|
+
case "H264":
|
|
76
|
+
case "AVC":
|
|
77
|
+
case "AVC1": {
|
|
78
78
|
// Try to extract profile/level from init data
|
|
79
79
|
const profileLevel = extractH264Profile(track.init);
|
|
80
|
-
return profileLevel ||
|
|
80
|
+
return profileLevel || "avc1.42E01E"; // Default: Baseline Profile, Level 3.0
|
|
81
81
|
}
|
|
82
|
-
case
|
|
83
|
-
case
|
|
84
|
-
case
|
|
85
|
-
case
|
|
82
|
+
case "H265":
|
|
83
|
+
case "HEVC":
|
|
84
|
+
case "HEV1":
|
|
85
|
+
case "HVC1": {
|
|
86
86
|
// Try to extract profile/level from init data
|
|
87
87
|
const profileLevel = extractHEVCProfile(track.init);
|
|
88
|
-
return profileLevel ||
|
|
88
|
+
return profileLevel || "hev1.1.6.L93.B0"; // Default: Main Profile, Level 3.1
|
|
89
89
|
}
|
|
90
|
-
case
|
|
91
|
-
return
|
|
92
|
-
case
|
|
93
|
-
return
|
|
94
|
-
case
|
|
95
|
-
return
|
|
96
|
-
case
|
|
97
|
-
return
|
|
90
|
+
case "VP8":
|
|
91
|
+
return "vp8";
|
|
92
|
+
case "VP9":
|
|
93
|
+
return "vp09.00.10.08"; // Profile 0, Level 1.0, 8-bit
|
|
94
|
+
case "AV1":
|
|
95
|
+
return "av01.0.01M.08"; // Main Profile, Level 2.1, 8-bit
|
|
96
|
+
case "THEORA":
|
|
97
|
+
return "theora";
|
|
98
98
|
default:
|
|
99
99
|
return codec.toLowerCase();
|
|
100
100
|
}
|
|
@@ -180,7 +180,7 @@ function extractHEVCProfile(init?: string): string | null {
|
|
|
180
180
|
const profileIdc = bytes[i];
|
|
181
181
|
if (profileIdc >= 1 && profileIdc <= 5) {
|
|
182
182
|
// Valid profile IDC (1=Main, 2=Main10, 3=MainStill, 4=Range Extensions, 5=High Throughput)
|
|
183
|
-
|
|
183
|
+
// tierFlag assumed to be 0 (main tier)
|
|
184
184
|
const levelIdc = bytes[i + 1] || 93; // Default to level 3.1
|
|
185
185
|
|
|
186
186
|
// Format: hev1.{profile}.{tier_flag}{compatibility}.L{level}.{constraints}
|
|
@@ -199,7 +199,7 @@ function extractHEVCProfile(init?: string): string | null {
|
|
|
199
199
|
* Convert byte to 2-digit hex string
|
|
200
200
|
*/
|
|
201
201
|
function toHex(byte: number): string {
|
|
202
|
-
return byte.toString(16).padStart(2,
|
|
202
|
+
return byte.toString(16).padStart(2, "0").toUpperCase();
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
@@ -221,8 +221,8 @@ function base64ToBytes(base64: string): Uint8Array {
|
|
|
221
221
|
* @param containerType - Container type (default: 'video/mp4')
|
|
222
222
|
* @returns true if supported
|
|
223
223
|
*/
|
|
224
|
-
export function isCodecSupported(codecString: string, containerType =
|
|
225
|
-
if (typeof MediaSource ===
|
|
224
|
+
export function isCodecSupported(codecString: string, containerType = "video/mp4"): boolean {
|
|
225
|
+
if (typeof MediaSource === "undefined" || !MediaSource.isTypeSupported) {
|
|
226
226
|
return false;
|
|
227
227
|
}
|
|
228
228
|
|
|
@@ -237,8 +237,11 @@ export function isCodecSupported(codecString: string, containerType = 'video/mp4
|
|
|
237
237
|
* @param type - Track type to filter ('video' or 'audio')
|
|
238
238
|
* @returns Best supported track or null
|
|
239
239
|
*/
|
|
240
|
-
export function getBestSupportedTrack(
|
|
241
|
-
|
|
240
|
+
export function getBestSupportedTrack(
|
|
241
|
+
tracks: TrackInfo[],
|
|
242
|
+
type: "video" | "audio"
|
|
243
|
+
): TrackInfo | null {
|
|
244
|
+
const filteredTracks = tracks.filter((t) => t.type === type);
|
|
242
245
|
|
|
243
246
|
for (const track of filteredTracks) {
|
|
244
247
|
const codecString = translateCodec(track);
|
package/src/core/Disposable.ts
CHANGED
|
@@ -57,7 +57,7 @@ export abstract class BaseDisposable implements Disposable {
|
|
|
57
57
|
* Throw if this object has been disposed.
|
|
58
58
|
* Use at the start of methods that shouldn't run after disposal.
|
|
59
59
|
*/
|
|
60
|
-
protected throwIfDisposed(operation: string =
|
|
60
|
+
protected throwIfDisposed(operation: string = "operation"): void {
|
|
61
61
|
if (this._disposed) {
|
|
62
62
|
throw new Error(`Cannot perform ${operation} on disposed object`);
|
|
63
63
|
}
|
|
@@ -80,7 +80,7 @@ export function disposeAll(...disposables: (Disposable | null | undefined)[]): v
|
|
|
80
80
|
try {
|
|
81
81
|
d.dispose();
|
|
82
82
|
} catch (err) {
|
|
83
|
-
console.warn(
|
|
83
|
+
console.warn("[Disposable] Error during disposal:", err);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -104,13 +104,13 @@ export function createCompositeDisposable(
|
|
|
104
104
|
|
|
105
105
|
for (const d of disposables) {
|
|
106
106
|
try {
|
|
107
|
-
if (typeof d ===
|
|
107
|
+
if (typeof d === "function") {
|
|
108
108
|
d();
|
|
109
109
|
} else if (d && !d.disposed) {
|
|
110
110
|
d.dispose();
|
|
111
111
|
}
|
|
112
112
|
} catch (err) {
|
|
113
|
-
console.warn(
|
|
113
|
+
console.warn("[CompositeDisposable] Error during disposal:", err);
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
},
|
package/src/core/EventEmitter.ts
CHANGED
|
@@ -77,7 +77,7 @@ export class TypedEventEmitter<Events extends Record<string, any>> {
|
|
|
77
77
|
* @param data - The event payload
|
|
78
78
|
*/
|
|
79
79
|
protected emit<K extends keyof Events>(event: K, data: Events[K]): void {
|
|
80
|
-
this.listeners.get(event)?.forEach(listener => {
|
|
80
|
+
this.listeners.get(event)?.forEach((listener) => {
|
|
81
81
|
try {
|
|
82
82
|
listener(data);
|
|
83
83
|
} catch (e) {
|
|
@@ -5,22 +5,22 @@
|
|
|
5
5
|
* Extracted from useViewerEndpoints.ts for use in headless core.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { TypedEventEmitter } from
|
|
9
|
-
import type { ContentEndpoints, ContentType } from
|
|
8
|
+
import { TypedEventEmitter } from "./EventEmitter";
|
|
9
|
+
import type { ContentEndpoints, ContentType } from "../types";
|
|
10
10
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Types
|
|
13
13
|
// ============================================================================
|
|
14
14
|
|
|
15
|
-
export type GatewayStatus =
|
|
15
|
+
export type GatewayStatus = "idle" | "loading" | "ready" | "error";
|
|
16
16
|
|
|
17
17
|
export interface GatewayClientConfig {
|
|
18
18
|
/** Gateway GraphQL endpoint URL */
|
|
19
19
|
gatewayUrl: string;
|
|
20
|
-
/** Content type to resolve */
|
|
21
|
-
contentType: ContentType;
|
|
22
20
|
/** Content identifier (stream name) */
|
|
23
21
|
contentId: string;
|
|
22
|
+
/** Optional content type (no longer required for resolution) */
|
|
23
|
+
contentType?: ContentType;
|
|
24
24
|
/** Optional auth token for private streams */
|
|
25
25
|
authToken?: string;
|
|
26
26
|
/** Maximum retry attempts (default: 3) */
|
|
@@ -45,14 +45,14 @@ const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
|
45
45
|
// F2: Cache TTL for resolved endpoints
|
|
46
46
|
const DEFAULT_CACHE_TTL_MS = 10000;
|
|
47
47
|
// F3: Circuit breaker constants
|
|
48
|
-
const CIRCUIT_BREAKER_THRESHOLD = 5;
|
|
49
|
-
const CIRCUIT_BREAKER_TIMEOUT_MS = 30000;
|
|
48
|
+
const CIRCUIT_BREAKER_THRESHOLD = 5; // Open after 5 consecutive failures
|
|
49
|
+
const CIRCUIT_BREAKER_TIMEOUT_MS = 30000; // Half-open after 30 seconds
|
|
50
50
|
|
|
51
|
-
type CircuitBreakerState =
|
|
51
|
+
type CircuitBreakerState = "closed" | "open" | "half-open";
|
|
52
52
|
|
|
53
53
|
const RESOLVE_VIEWER_QUERY = `
|
|
54
|
-
query ResolveViewer($
|
|
55
|
-
resolveViewerEndpoint(
|
|
54
|
+
query ResolveViewer($contentId: String!) {
|
|
55
|
+
resolveViewerEndpoint(contentId: $contentId) {
|
|
56
56
|
primary { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
57
57
|
fallbacks { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
58
58
|
metadata { contentType contentId title description durationSeconds status isLive viewers recordingSizeBytes clipSource createdAt }
|
|
@@ -80,7 +80,7 @@ async function fetchWithRetry(
|
|
|
80
80
|
const response = await fetch(url, options);
|
|
81
81
|
return response;
|
|
82
82
|
} catch (e) {
|
|
83
|
-
lastError = e instanceof Error ? e : new Error(
|
|
83
|
+
lastError = e instanceof Error ? e : new Error("Fetch failed");
|
|
84
84
|
|
|
85
85
|
// Don't retry on abort
|
|
86
86
|
if (options.signal?.aborted) {
|
|
@@ -91,12 +91,12 @@ async function fetchWithRetry(
|
|
|
91
91
|
if (attempt < maxRetries - 1) {
|
|
92
92
|
const delay = initialDelay * Math.pow(2, attempt);
|
|
93
93
|
console.warn(`[GatewayClient] Retry ${attempt + 1}/${maxRetries - 1} after ${delay}ms`);
|
|
94
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
throw lastError ?? new Error(
|
|
99
|
+
throw lastError ?? new Error("Gateway unreachable after retries");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// ============================================================================
|
|
@@ -110,8 +110,7 @@ async function fetchWithRetry(
|
|
|
110
110
|
* ```typescript
|
|
111
111
|
* const client = new GatewayClient({
|
|
112
112
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
113
|
-
*
|
|
114
|
-
* contentId: 'my-stream',
|
|
113
|
+
* contentId: 'pk_...', // playbackId (view key)
|
|
115
114
|
* });
|
|
116
115
|
*
|
|
117
116
|
* client.on('statusChange', ({ status }) => console.log('Status:', status));
|
|
@@ -122,7 +121,7 @@ async function fetchWithRetry(
|
|
|
122
121
|
*/
|
|
123
122
|
export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
124
123
|
private config: GatewayClientConfig;
|
|
125
|
-
private status: GatewayStatus =
|
|
124
|
+
private status: GatewayStatus = "idle";
|
|
126
125
|
private endpoints: ContentEndpoints | null = null;
|
|
127
126
|
private error: string | null = null;
|
|
128
127
|
private abortController: AbortController | null = null;
|
|
@@ -134,7 +133,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
134
133
|
private cacheTtlMs: number;
|
|
135
134
|
|
|
136
135
|
// F3: Circuit breaker state
|
|
137
|
-
private circuitState: CircuitBreakerState =
|
|
136
|
+
private circuitState: CircuitBreakerState = "closed";
|
|
138
137
|
private consecutiveFailures = 0;
|
|
139
138
|
private circuitOpenedAt = 0;
|
|
140
139
|
|
|
@@ -160,7 +159,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
160
159
|
|
|
161
160
|
// F3: Check circuit breaker
|
|
162
161
|
if (!this.canAttemptRequest()) {
|
|
163
|
-
throw new Error(
|
|
162
|
+
throw new Error("Circuit breaker is open - too many recent failures");
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
// F2: Return in-flight request if one exists (deduplication)
|
|
@@ -215,18 +214,18 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
215
214
|
*/
|
|
216
215
|
private canAttemptRequest(): boolean {
|
|
217
216
|
switch (this.circuitState) {
|
|
218
|
-
case
|
|
217
|
+
case "closed":
|
|
219
218
|
return true;
|
|
220
219
|
|
|
221
|
-
case
|
|
220
|
+
case "open":
|
|
222
221
|
// Check if enough time has passed to try half-open
|
|
223
222
|
if (Date.now() - this.circuitOpenedAt >= CIRCUIT_BREAKER_TIMEOUT_MS) {
|
|
224
|
-
this.circuitState =
|
|
223
|
+
this.circuitState = "half-open";
|
|
225
224
|
return true;
|
|
226
225
|
}
|
|
227
226
|
return false;
|
|
228
227
|
|
|
229
|
-
case
|
|
228
|
+
case "half-open":
|
|
230
229
|
// Allow one request to test the circuit
|
|
231
230
|
return true;
|
|
232
231
|
}
|
|
@@ -237,7 +236,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
237
236
|
*/
|
|
238
237
|
private onSuccess(): void {
|
|
239
238
|
this.consecutiveFailures = 0;
|
|
240
|
-
this.circuitState =
|
|
239
|
+
this.circuitState = "closed";
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
/**
|
|
@@ -246,15 +245,17 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
246
245
|
private onFailure(): void {
|
|
247
246
|
this.consecutiveFailures++;
|
|
248
247
|
|
|
249
|
-
if (this.circuitState ===
|
|
248
|
+
if (this.circuitState === "half-open") {
|
|
250
249
|
// Failed during half-open - re-open the circuit
|
|
251
|
-
this.circuitState =
|
|
250
|
+
this.circuitState = "open";
|
|
252
251
|
this.circuitOpenedAt = Date.now();
|
|
253
252
|
} else if (this.consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
254
253
|
// Threshold reached - open the circuit
|
|
255
|
-
this.circuitState =
|
|
254
|
+
this.circuitState = "open";
|
|
256
255
|
this.circuitOpenedAt = Date.now();
|
|
257
|
-
console.warn(
|
|
256
|
+
console.warn(
|
|
257
|
+
`[GatewayClient] Circuit breaker opened after ${this.consecutiveFailures} consecutive failures`
|
|
258
|
+
);
|
|
258
259
|
}
|
|
259
260
|
}
|
|
260
261
|
|
|
@@ -265,7 +266,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
265
266
|
return {
|
|
266
267
|
state: this.circuitState,
|
|
267
268
|
failures: this.consecutiveFailures,
|
|
268
|
-
openedAt: this.circuitState ===
|
|
269
|
+
openedAt: this.circuitState === "open" ? this.circuitOpenedAt : null,
|
|
269
270
|
};
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -273,7 +274,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
273
274
|
* F3: Manually reset the circuit breaker
|
|
274
275
|
*/
|
|
275
276
|
resetCircuitBreaker(): void {
|
|
276
|
-
this.circuitState =
|
|
277
|
+
this.circuitState = "closed";
|
|
277
278
|
this.consecutiveFailures = 0;
|
|
278
279
|
this.circuitOpenedAt = 0;
|
|
279
280
|
}
|
|
@@ -288,7 +289,6 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
288
289
|
|
|
289
290
|
const {
|
|
290
291
|
gatewayUrl,
|
|
291
|
-
contentType,
|
|
292
292
|
contentId,
|
|
293
293
|
authToken,
|
|
294
294
|
maxRetries = DEFAULT_MAX_RETRIES,
|
|
@@ -296,31 +296,31 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
296
296
|
} = this.config;
|
|
297
297
|
|
|
298
298
|
// Validate required params
|
|
299
|
-
if (!gatewayUrl || !
|
|
300
|
-
const error =
|
|
301
|
-
this.setStatus(
|
|
299
|
+
if (!gatewayUrl || !contentId) {
|
|
300
|
+
const error = "Missing required parameters: gatewayUrl or contentId";
|
|
301
|
+
this.setStatus("error", error);
|
|
302
302
|
throw new Error(error);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
this.setStatus(
|
|
305
|
+
this.setStatus("loading");
|
|
306
306
|
|
|
307
307
|
const ac = new AbortController();
|
|
308
308
|
this.abortController = ac;
|
|
309
309
|
|
|
310
310
|
try {
|
|
311
|
-
const graphqlEndpoint = gatewayUrl.replace(/\/$/,
|
|
311
|
+
const graphqlEndpoint = gatewayUrl.replace(/\/$/, "");
|
|
312
312
|
|
|
313
313
|
const res = await fetchWithRetry(
|
|
314
314
|
graphqlEndpoint,
|
|
315
315
|
{
|
|
316
|
-
method:
|
|
316
|
+
method: "POST",
|
|
317
317
|
headers: {
|
|
318
|
-
|
|
318
|
+
"Content-Type": "application/json",
|
|
319
319
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
320
320
|
},
|
|
321
321
|
body: JSON.stringify({
|
|
322
322
|
query: RESOLVE_VIEWER_QUERY,
|
|
323
|
-
variables: {
|
|
323
|
+
variables: { contentId },
|
|
324
324
|
}),
|
|
325
325
|
signal: ac.signal,
|
|
326
326
|
},
|
|
@@ -335,7 +335,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
335
335
|
const payload = await res.json();
|
|
336
336
|
|
|
337
337
|
if (payload.errors?.length) {
|
|
338
|
-
throw new Error(payload.errors[0]?.message ||
|
|
338
|
+
throw new Error(payload.errors[0]?.message || "GraphQL error");
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
const resp = payload.data?.resolveViewerEndpoint;
|
|
@@ -343,7 +343,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
343
343
|
const fallbacks = Array.isArray(resp?.fallbacks) ? resp.fallbacks : [];
|
|
344
344
|
|
|
345
345
|
if (!primary) {
|
|
346
|
-
throw new Error(
|
|
346
|
+
throw new Error("No endpoints available");
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
const endpoints: ContentEndpoints = {
|
|
@@ -355,19 +355,19 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
355
355
|
this.endpoints = endpoints;
|
|
356
356
|
// F2: Update cache timestamp
|
|
357
357
|
this.cacheTimestamp = Date.now();
|
|
358
|
-
this.setStatus(
|
|
359
|
-
this.emit(
|
|
358
|
+
this.setStatus("ready");
|
|
359
|
+
this.emit("endpointsResolved", { endpoints });
|
|
360
360
|
|
|
361
361
|
return endpoints;
|
|
362
362
|
} catch (e) {
|
|
363
363
|
// Ignore abort errors
|
|
364
364
|
if (ac.signal.aborted) {
|
|
365
|
-
throw new Error(
|
|
365
|
+
throw new Error("Request aborted");
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
const message = e instanceof Error ? e.message :
|
|
369
|
-
console.error(
|
|
370
|
-
this.setStatus(
|
|
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
371
|
throw new Error(message);
|
|
372
372
|
}
|
|
373
373
|
}
|
|
@@ -418,7 +418,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
418
418
|
this.inFlightRequest = null;
|
|
419
419
|
// F3: Reset circuit breaker for new config
|
|
420
420
|
this.resetCircuitBreaker();
|
|
421
|
-
this.setStatus(
|
|
421
|
+
this.setStatus("idle");
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
/**
|
|
@@ -432,7 +432,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
|
|
|
432
432
|
private setStatus(status: GatewayStatus, error?: string): void {
|
|
433
433
|
this.status = status;
|
|
434
434
|
this.error = error ?? null;
|
|
435
|
-
this.emit(
|
|
435
|
+
this.emit("statusChange", { status, error });
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
|