-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathadSegmentFixtureProvider.ts
More file actions
289 lines (252 loc) · 8.63 KB
/
Copy pathadSegmentFixtureProvider.ts
File metadata and controls
289 lines (252 loc) · 8.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/**
* Fixture-Based Ad Segment Provider (POC)
*
* Loads ad segments and tagger profiles from local JSON fixture files.
* This is the POC implementation - in production, this will be replaced
* with NostrAdSegmentProvider that queries Nostr relays.
*
* FIXTURE LOCATIONS:
* - Ad segments: /lib/fixtures/adSegments/{episodeKey}.json
* - Tagger profiles: /lib/fixtures/profiles/taggers.json
*
* DESIGN NOTES:
* - Uses dynamic imports to load JSON files
* - Implements AdSegmentProvider interface for swappability
* - Includes in-memory cache for performance
* - Gracefully handles missing fixtures (returns empty arrays/null)
*/
import {
AdSegment,
TaggerProfile,
EpisodeAdData,
AdSegmentProvider,
ChapterData,
} from './adSegmentTypes'
import { isHashBasedKey } from './adSegmentIdentity'
/**
* Fixture Ad Segment Provider
*
* POC implementation that reads from local JSON files.
* Implements AdSegmentProvider interface.
*/
export class FixtureAdSegmentProvider implements AdSegmentProvider {
private adSegmentCache: Map<string, AdSegment[]> = new Map()
private taggerProfileCache: Map<string, TaggerProfile | null> = new Map()
private episodeAdDataCache: Map<string, EpisodeAdData | null> = new Map()
/**
* Fetch Ad Segments for Episode
*
* Loads from: /lib/fixtures/adSegments/{episodeKey}.json
*
* @param episodeKey Episode identifier (GUID or hash)
* @returns Promise resolving to ad segments array (empty if not found)
*/
async fetchAdSegments(episodeKey: string): Promise<AdSegment[]> {
// Check cache first
if (this.adSegmentCache.has(episodeKey)) {
return this.adSegmentCache.get(episodeKey)!
}
try {
const data = await this.loadEpisodeAdData(episodeKey)
const segments = data?.adSegments || []
// Cache result
this.adSegmentCache.set(episodeKey, segments)
return segments
} catch (error) {
console.warn(`[FixtureProvider] Failed to load ad segments for episode "${episodeKey}":`, error)
// Cache empty result to avoid repeated failed attempts
this.adSegmentCache.set(episodeKey, [])
return []
}
}
/**
* Fetch Tagger Profile
*
* Loads from: /lib/fixtures/profiles/taggers.json
*
* @param pubkey Nostr public key (npub or hex)
* @returns Promise resolving to profile (null if not found)
*/
async fetchTaggerProfile(pubkey: string): Promise<TaggerProfile | null> {
// Check cache first
if (this.taggerProfileCache.has(pubkey)) {
return this.taggerProfileCache.get(pubkey)!
}
try {
const profiles = await this.loadTaggerProfiles()
const profile = profiles.find((p) => p.pubkey === pubkey) || null
// Cache result
this.taggerProfileCache.set(pubkey, profile)
return profile
} catch (error) {
console.warn(`[FixtureProvider] Failed to load tagger profile for "${pubkey}":`, error)
// Cache null result
this.taggerProfileCache.set(pubkey, null)
return null
}
}
/**
* Fetch Complete Episode Ad Data
*
* Loads both ad segments and metadata from fixture file.
*
* @param episodeKey Episode identifier
* @returns Promise resolving to episode ad data (null if not found)
*/
async fetchEpisodeAdData(episodeKey: string): Promise<EpisodeAdData | null> {
// Check cache first
if (this.episodeAdDataCache.has(episodeKey)) {
return this.episodeAdDataCache.get(episodeKey)!
}
try {
const data = await this.loadEpisodeAdData(episodeKey)
// Cache result
this.episodeAdDataCache.set(episodeKey, data)
return data
} catch (error) {
console.warn(`[FixtureProvider] Failed to load episode ad data for "${episodeKey}":`, error)
// Cache null result
this.episodeAdDataCache.set(episodeKey, null)
return null
}
}
/**
* Load Episode Ad Data from Fixture File
*
* Attempts to load from /lib/fixtures/adSegments/{episodeKey}.json
*
* For hash-based keys (format: "hash:abc123..."), only uses the hash portion
* as the filename (without "hash:" prefix).
*
* @param episodeKey Episode identifier
* @returns Episode ad data or null if not found
*/
private async loadEpisodeAdData(episodeKey: string): Promise<EpisodeAdData | null> {
// Sanitize episode key for use as filename
const filename = this.sanitizeEpisodeKeyForFilename(episodeKey)
try {
// Attempt to dynamically import the fixture file
// Note: In Next.js, this works for files in public/ or lib/
const data = await import(`~/lib/fixtures/adSegments/${filename}.json`)
// Validate basic structure
if (!data || !Array.isArray(data.adSegments)) {
console.warn(`[FixtureProvider] Invalid fixture format for "${filename}.json"`)
return null
}
return data as EpisodeAdData
} catch (error) {
// File not found or import failed - this is expected for episodes without ad data
return null
}
}
/**
* Load Tagger Profiles from Fixture File
*
* Loads from: /lib/fixtures/profiles/taggers.json
*
* @returns Array of tagger profiles
*/
private async loadTaggerProfiles(): Promise<TaggerProfile[]> {
try {
const data = await import('~/lib/fixtures/profiles/taggers.json')
if (!data || !Array.isArray(data.profiles)) {
console.warn('[FixtureProvider] Invalid tagger profiles fixture format')
return []
}
return data.profiles as TaggerProfile[]
} catch (error) {
console.error('[FixtureProvider] Failed to load tagger profiles:', error)
return []
}
}
/**
* Sanitize Episode Key for Filename
*
* Converts episode key to a safe filename:
* - Hash-based keys: strip "hash:" prefix, use hash only
* - GUID-based keys: replace unsafe characters with underscores
*
* Examples:
* - "hash:abc123def456" → "abc123def456"
* - "https://example.com/episode/123" → "https___example_com_episode_123"
* - "urn:uuid:12345" → "urn_uuid_12345"
*
* @param episodeKey Episode identifier
* @returns Safe filename (without .json extension)
*/
private sanitizeEpisodeKeyForFilename(episodeKey: string): string {
// For hash-based keys, strip the "hash:" prefix
if (isHashBasedKey(episodeKey)) {
return episodeKey.substring(5) // Remove "hash:" prefix
}
// For GUID-based keys, replace unsafe filename characters
// Replace: / \ : * ? " < > | with underscores
return episodeKey.replace(/[/\\:*?"<>|]/g, '_')
}
/**
* Clear Cache
*
* Useful for testing or forcing reload of fixtures.
*/
clearCache(): void {
this.adSegmentCache.clear()
this.taggerProfileCache.clear()
this.episodeAdDataCache.clear()
}
/**
* Preload Fixtures
*
* Preloads fixtures for multiple episodes (useful for prefetching).
*
* @param episodeKeys Array of episode keys to preload
*/
async preloadFixtures(episodeKeys: string[]): Promise<void> {
await Promise.all(episodeKeys.map((key) => this.fetchAdSegments(key)))
}
}
/**
* Singleton Instance
*
* Export a singleton instance for convenience.
* Can be replaced with dependency injection in the future.
*/
export const fixtureAdSegmentProvider = new FixtureAdSegmentProvider()
/**
* Create Mock Provider (for Testing)
*
* Creates a provider with mock data for unit tests.
*
* @param mockData Mock ad segments and profiles
* @returns Mock provider instance
*/
export function createMockAdSegmentProvider(mockData: {
adSegments?: Record<string, AdSegment[]>
profiles?: TaggerProfile[]
}): AdSegmentProvider {
return {
async fetchAdSegments(episodeKey: string): Promise<AdSegment[]> {
return mockData.adSegments?.[episodeKey] || []
},
async fetchTaggerProfile(pubkey: string): Promise<TaggerProfile | null> {
return mockData.profiles?.find((p) => p.pubkey === pubkey) || null
},
async fetchEpisodeAdData(episodeKey: string): Promise<EpisodeAdData | null> {
const segments = mockData.adSegments?.[episodeKey]
if (!segments) return null
return {
episodeKey,
adSegments: segments,
}
},
}
}
// TODO: Future Nostr Integration Points
// - Implement NostrAdSegmentProvider class
// - Query Nostr relays for ad segment events (define custom NIP or use NIP-XX)
// - Implement relay connection pooling and caching
// - Add support for real-time updates via WebSocket subscriptions
// - Integrate with Nostr profile fetching (NIP-01 kind 0 events)
// - Add support for zap-based validation (NIP-57)
// - Consider implementing provider registry pattern for easy swapping:
// providerRegistry.register('nostr', new NostrAdSegmentProvider())
// providerRegistry.setActive('nostr')