@xhub-short/adapters 0.1.0-beta.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/dist/index.js ADDED
@@ -0,0 +1,3238 @@
1
+ // src/data/mock.ts
2
+ var MOCK_VIDEOS = [
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ // HLS Videos (for testing hls.js integration)
5
+ // ═══════════════════════════════════════════════════════════════════════════
6
+ {
7
+ id: "video-1",
8
+ source: {
9
+ url: "https://peertube.teknix.services/static/streaming-playlists/hls/dd8de71d-0b75-4677-a1a2-6f60e673bee4/465faffa-6d08-4f34-ae40-691cc904ce7b-master.m3u8",
10
+ type: "hls"
11
+ },
12
+ poster: "https://images.unsplash.com/photo-1536240478700-b869070f9279?w=400&h=600&fit=crop",
13
+ duration: 634,
14
+ title: "HLS Test Stream - Big Buck Bunny",
15
+ author: {
16
+ id: "author-1",
17
+ name: "Mux Test Streams",
18
+ avatar: "https://i.pravatar.cc/150?u=author1",
19
+ isVerified: true
20
+ },
21
+ stats: {
22
+ views: 1234567,
23
+ likes: 98765,
24
+ comments: 4321,
25
+ shares: 876
26
+ },
27
+ isLiked: false,
28
+ isFollowing: false,
29
+ createdAt: "2024-01-15T10:30:00Z",
30
+ hashtags: ["hls", "streaming", "test"]
31
+ },
32
+ {
33
+ id: "video-2",
34
+ source: {
35
+ url: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8",
36
+ type: "hls"
37
+ },
38
+ poster: "https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=400&h=600&fit=crop",
39
+ duration: 120,
40
+ title: "Apple HLS Sample - Advanced Stream",
41
+ author: {
42
+ id: "author-2",
43
+ name: "Apple Developer",
44
+ avatar: "https://i.pravatar.cc/150?u=author2",
45
+ isVerified: true
46
+ },
47
+ stats: {
48
+ views: 876543,
49
+ likes: 54321,
50
+ comments: 2109,
51
+ shares: 543
52
+ },
53
+ isLiked: true,
54
+ isFollowing: true,
55
+ createdAt: "2024-01-14T15:45:00Z",
56
+ hashtags: ["apple", "hls", "fmp4"]
57
+ },
58
+ // ═══════════════════════════════════════════════════════════════════════════
59
+ // MP4 Videos
60
+ // ═══════════════════════════════════════════════════════════════════════════
61
+ {
62
+ id: "video-3",
63
+ source: {
64
+ url: "https://peertube.teknix.services/static/streaming-playlists/hls/ea58b245-b3bf-4958-b2f0-b31f8113d142/83f1bb91-e76a-4dcd-8034-2ec37cb70ead-master.m3u8",
65
+ type: "hls"
66
+ },
67
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerBlazes.jpg",
68
+ duration: 15,
69
+ title: "For Bigger Blazes (MP4)",
70
+ author: {
71
+ id: "author-3",
72
+ name: "Google Chrome",
73
+ avatar: "https://i.pravatar.cc/150?u=author3",
74
+ isVerified: true
75
+ },
76
+ stats: {
77
+ views: 543210,
78
+ likes: 32109,
79
+ comments: 1543,
80
+ shares: 321
81
+ },
82
+ isLiked: false,
83
+ isFollowing: false,
84
+ createdAt: "2024-01-13T09:20:00Z",
85
+ hashtags: ["chrome", "blazes"]
86
+ },
87
+ {
88
+ id: "video-4",
89
+ source: {
90
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
91
+ type: "mp4"
92
+ },
93
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerEscapes.jpg",
94
+ duration: 15,
95
+ title: "For Bigger Escapes",
96
+ author: {
97
+ id: "author-3",
98
+ name: "Google Chrome",
99
+ avatar: "https://i.pravatar.cc/150?u=author3",
100
+ isVerified: true
101
+ },
102
+ stats: {
103
+ views: 432109,
104
+ likes: 21098,
105
+ comments: 987,
106
+ shares: 210
107
+ },
108
+ isLiked: false,
109
+ isFollowing: false,
110
+ createdAt: "2024-01-12T14:10:00Z",
111
+ hashtags: ["chrome", "escapes"]
112
+ },
113
+ {
114
+ id: "video-5",
115
+ source: {
116
+ url: "https://peertube.teknix.services/static/streaming-playlists/hls/003a41a3-25c1-419b-9548-7a1597adc85f/9cc93564-c9c9-4107-ae12-cd22d0db8046-master.m3u8",
117
+ type: "hls"
118
+ },
119
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerFun.jpg",
120
+ duration: 60,
121
+ title: "For Bigger Fun",
122
+ author: {
123
+ id: "author-3",
124
+ name: "Google Chrome",
125
+ avatar: "https://i.pravatar.cc/150?u=author3",
126
+ isVerified: true
127
+ },
128
+ stats: {
129
+ views: 321098,
130
+ likes: 10987,
131
+ comments: 654,
132
+ shares: 109
133
+ },
134
+ isLiked: true,
135
+ isFollowing: false,
136
+ createdAt: "2024-01-11T11:05:00Z",
137
+ hashtags: ["chrome", "fun"]
138
+ },
139
+ {
140
+ id: "video-6",
141
+ source: {
142
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
143
+ type: "mp4"
144
+ },
145
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerJoyrides.jpg",
146
+ duration: 15,
147
+ title: "For Bigger Joyrides",
148
+ author: {
149
+ id: "author-4",
150
+ name: "GoPro Adventures",
151
+ avatar: "https://i.pravatar.cc/150?u=author4",
152
+ isVerified: true
153
+ },
154
+ stats: {
155
+ views: 892345,
156
+ likes: 67890,
157
+ comments: 3456,
158
+ shares: 789
159
+ },
160
+ isLiked: false,
161
+ isFollowing: true,
162
+ createdAt: "2024-01-10T16:45:00Z",
163
+ hashtags: ["adventure", "joyride", "travel"]
164
+ },
165
+ {
166
+ id: "video-7",
167
+ source: {
168
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
169
+ type: "mp4"
170
+ },
171
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerMeltdowns.jpg",
172
+ duration: 15,
173
+ title: "For Bigger Meltdowns",
174
+ author: {
175
+ id: "author-5",
176
+ name: "Ice Cream Factory",
177
+ avatar: "https://i.pravatar.cc/150?u=author5",
178
+ isVerified: false
179
+ },
180
+ stats: {
181
+ views: 234567,
182
+ likes: 12345,
183
+ comments: 567,
184
+ shares: 123
185
+ },
186
+ isLiked: true,
187
+ isFollowing: false,
188
+ createdAt: "2024-01-09T12:30:00Z",
189
+ hashtags: ["satisfying", "icecream", "asmr"]
190
+ },
191
+ {
192
+ id: "video-8",
193
+ source: {
194
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
195
+ type: "mp4"
196
+ },
197
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/Sintel.jpg",
198
+ duration: 888,
199
+ title: "Sintel - Epic Fantasy Short Film",
200
+ author: {
201
+ id: "author-1",
202
+ name: "Blender Foundation",
203
+ avatar: "https://i.pravatar.cc/150?u=author1",
204
+ isVerified: true
205
+ },
206
+ stats: {
207
+ views: 2567890,
208
+ likes: 189012,
209
+ comments: 8901,
210
+ shares: 2345
211
+ },
212
+ isLiked: false,
213
+ isFollowing: false,
214
+ createdAt: "2024-01-08T09:15:00Z",
215
+ hashtags: ["fantasy", "animation", "sintel", "blender"]
216
+ },
217
+ {
218
+ id: "video-9",
219
+ source: {
220
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4",
221
+ type: "mp4"
222
+ },
223
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/SubaruOutbackOnStreetAndDirt.jpg",
224
+ duration: 596,
225
+ title: "Subaru Outback - Street & Dirt",
226
+ author: {
227
+ id: "author-6",
228
+ name: "Car Reviews Daily",
229
+ avatar: "https://i.pravatar.cc/150?u=author6",
230
+ isVerified: true
231
+ },
232
+ stats: {
233
+ views: 456789,
234
+ likes: 34567,
235
+ comments: 1234,
236
+ shares: 456
237
+ },
238
+ isLiked: false,
239
+ isFollowing: true,
240
+ createdAt: "2024-01-07T14:20:00Z",
241
+ hashtags: ["cars", "subaru", "offroad", "review"]
242
+ },
243
+ {
244
+ id: "video-10",
245
+ source: {
246
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
247
+ type: "mp4"
248
+ },
249
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/TearsOfSteel.jpg",
250
+ duration: 734,
251
+ title: "Tears of Steel - Sci-Fi Drama",
252
+ author: {
253
+ id: "author-1",
254
+ name: "Blender Foundation",
255
+ avatar: "https://i.pravatar.cc/150?u=author1",
256
+ isVerified: true
257
+ },
258
+ stats: {
259
+ views: 1876543,
260
+ likes: 145678,
261
+ comments: 6789,
262
+ shares: 1890
263
+ },
264
+ isLiked: true,
265
+ isFollowing: false,
266
+ createdAt: "2024-01-06T11:10:00Z",
267
+ hashtags: ["scifi", "drama", "blender", "vfx"]
268
+ },
269
+ {
270
+ id: "video-11",
271
+ source: {
272
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4",
273
+ type: "mp4"
274
+ },
275
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/VolkswagenGTIReview.jpg",
276
+ duration: 596,
277
+ title: "VW GTI Review - Hot Hatch King",
278
+ author: {
279
+ id: "author-6",
280
+ name: "Car Reviews Daily",
281
+ avatar: "https://i.pravatar.cc/150?u=author6",
282
+ isVerified: true
283
+ },
284
+ stats: {
285
+ views: 567890,
286
+ likes: 45678,
287
+ comments: 2345,
288
+ shares: 567
289
+ },
290
+ isLiked: false,
291
+ isFollowing: true,
292
+ createdAt: "2024-01-05T17:35:00Z",
293
+ hashtags: ["cars", "vw", "gti", "hothatch"]
294
+ },
295
+ {
296
+ id: "video-12",
297
+ source: {
298
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4",
299
+ type: "mp4"
300
+ },
301
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/WeAreGoingOnBullrun.jpg",
302
+ duration: 596,
303
+ title: "We Are Going On Bullrun! \u{1F3CE}\uFE0F",
304
+ author: {
305
+ id: "author-7",
306
+ name: "Rally Life",
307
+ avatar: "https://i.pravatar.cc/150?u=author7",
308
+ isVerified: false
309
+ },
310
+ stats: {
311
+ views: 345678,
312
+ likes: 23456,
313
+ comments: 890,
314
+ shares: 234
315
+ },
316
+ isLiked: true,
317
+ isFollowing: false,
318
+ createdAt: "2024-01-04T08:50:00Z",
319
+ hashtags: ["rally", "racing", "bullrun", "supercars"]
320
+ },
321
+ {
322
+ id: "video-13",
323
+ source: {
324
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4",
325
+ type: "mp4"
326
+ },
327
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/WhatCarCanYouGetForAGrand.jpg",
328
+ duration: 596,
329
+ title: "What Car Can You Get For $1000?",
330
+ author: {
331
+ id: "author-8",
332
+ name: "Budget Car Tips",
333
+ avatar: "https://i.pravatar.cc/150?u=author8",
334
+ isVerified: false
335
+ },
336
+ stats: {
337
+ views: 789012,
338
+ likes: 56789,
339
+ comments: 3456,
340
+ shares: 890
341
+ },
342
+ isLiked: false,
343
+ isFollowing: false,
344
+ createdAt: "2024-01-03T13:25:00Z",
345
+ hashtags: ["budget", "usedcars", "tips", "bargain"]
346
+ },
347
+ {
348
+ id: "video-14",
349
+ source: {
350
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
351
+ type: "mp4"
352
+ },
353
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg",
354
+ duration: 596,
355
+ title: "Behind The Scenes: Big Buck Bunny",
356
+ author: {
357
+ id: "author-9",
358
+ name: "3D Animation Pro",
359
+ avatar: "https://i.pravatar.cc/150?u=author9",
360
+ isVerified: true
361
+ },
362
+ stats: {
363
+ views: 456123,
364
+ likes: 34512,
365
+ comments: 1567,
366
+ shares: 412
367
+ },
368
+ isLiked: false,
369
+ isFollowing: true,
370
+ createdAt: "2024-01-02T10:40:00Z",
371
+ hashtags: ["bts", "animation", "3d", "making"]
372
+ },
373
+ {
374
+ id: "video-15",
375
+ source: {
376
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
377
+ type: "mp4"
378
+ },
379
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ElephantsDream.jpg",
380
+ duration: 653,
381
+ title: "Making of Elephant's Dream",
382
+ author: {
383
+ id: "author-9",
384
+ name: "3D Animation Pro",
385
+ avatar: "https://i.pravatar.cc/150?u=author9",
386
+ isVerified: true
387
+ },
388
+ stats: {
389
+ views: 234890,
390
+ likes: 19876,
391
+ comments: 876,
392
+ shares: 234
393
+ },
394
+ isLiked: true,
395
+ isFollowing: true,
396
+ createdAt: "2024-01-01T15:15:00Z",
397
+ hashtags: ["tutorial", "animation", "blender", "makingof"]
398
+ },
399
+ {
400
+ id: "video-16",
401
+ source: {
402
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
403
+ type: "mp4"
404
+ },
405
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/Sintel.jpg",
406
+ duration: 888,
407
+ title: "Sintel Character Design Breakdown",
408
+ author: {
409
+ id: "author-10",
410
+ name: "Character Artist Hub",
411
+ avatar: "https://i.pravatar.cc/150?u=author10",
412
+ isVerified: true
413
+ },
414
+ stats: {
415
+ views: 678901,
416
+ likes: 45678,
417
+ comments: 2134,
418
+ shares: 567
419
+ },
420
+ isLiked: false,
421
+ isFollowing: false,
422
+ createdAt: "2023-12-31T18:00:00Z",
423
+ hashtags: ["characterdesign", "sintel", "tutorial", "art"]
424
+ },
425
+ {
426
+ id: "video-17",
427
+ source: {
428
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
429
+ type: "mp4"
430
+ },
431
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/TearsOfSteel.jpg",
432
+ duration: 734,
433
+ title: "VFX Breakdown: Tears of Steel",
434
+ author: {
435
+ id: "author-11",
436
+ name: "VFX Masters",
437
+ avatar: "https://i.pravatar.cc/150?u=author11",
438
+ isVerified: true
439
+ },
440
+ stats: {
441
+ views: 890123,
442
+ likes: 67890,
443
+ comments: 3456,
444
+ shares: 789
445
+ },
446
+ isLiked: true,
447
+ isFollowing: true,
448
+ createdAt: "2023-12-30T09:30:00Z",
449
+ hashtags: ["vfx", "breakdown", "compositing", "cgi"]
450
+ },
451
+ {
452
+ id: "video-18",
453
+ source: {
454
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
455
+ type: "mp4"
456
+ },
457
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerJoyrides.jpg",
458
+ duration: 15,
459
+ title: "POV: Ultimate Road Trip \u{1F304}",
460
+ author: {
461
+ id: "author-12",
462
+ name: "Travel Vlogger",
463
+ avatar: "https://i.pravatar.cc/150?u=author12",
464
+ isVerified: false
465
+ },
466
+ stats: {
467
+ views: 123456,
468
+ likes: 9876,
469
+ comments: 432,
470
+ shares: 123
471
+ },
472
+ isLiked: false,
473
+ isFollowing: false,
474
+ createdAt: "2023-12-29T14:45:00Z",
475
+ hashtags: ["pov", "roadtrip", "travel", "wanderlust"]
476
+ },
477
+ {
478
+ id: "video-19",
479
+ source: {
480
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
481
+ type: "mp4"
482
+ },
483
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerBlazes.jpg",
484
+ duration: 15,
485
+ title: "Satisfying Fire Compilation \u{1F525}",
486
+ author: {
487
+ id: "author-13",
488
+ name: "Oddly Satisfying",
489
+ avatar: "https://i.pravatar.cc/150?u=author13",
490
+ isVerified: true
491
+ },
492
+ stats: {
493
+ views: 567890,
494
+ likes: 45678,
495
+ comments: 1234,
496
+ shares: 456
497
+ },
498
+ isLiked: true,
499
+ isFollowing: false,
500
+ createdAt: "2023-12-28T11:20:00Z",
501
+ hashtags: ["satisfying", "fire", "asmr", "relaxing"]
502
+ },
503
+ {
504
+ id: "video-20",
505
+ source: {
506
+ url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
507
+ type: "mp4"
508
+ },
509
+ poster: "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerMeltdowns.jpg",
510
+ duration: 15,
511
+ title: "Chocolate Factory Tour \u{1F36B}",
512
+ author: {
513
+ id: "author-14",
514
+ name: "Food Explorer",
515
+ avatar: "https://i.pravatar.cc/150?u=author14",
516
+ isVerified: false
517
+ },
518
+ stats: {
519
+ views: 345678,
520
+ likes: 23456,
521
+ comments: 876,
522
+ shares: 234
523
+ },
524
+ isLiked: false,
525
+ isFollowing: true,
526
+ createdAt: "2023-12-27T16:55:00Z",
527
+ hashtags: ["food", "chocolate", "factory", "tour"]
528
+ }
529
+ ];
530
+ var MockDataAdapter = class {
531
+ constructor(options = {}) {
532
+ this.videos = options.videos ?? MOCK_VIDEOS;
533
+ this.pageSize = options.pageSize ?? 3;
534
+ this.delay = options.delay ?? 300;
535
+ }
536
+ /**
537
+ * Fetch a page of mock videos
538
+ */
539
+ async fetchFeed(cursor) {
540
+ await this.simulateDelay();
541
+ const offset = cursor ? Number.parseInt(cursor, 10) : 0;
542
+ const start = offset;
543
+ const end = start + this.pageSize;
544
+ const items = this.videos.slice(start, end);
545
+ const hasMore = end < this.videos.length;
546
+ const nextCursor = hasMore ? String(end) : null;
547
+ return {
548
+ items,
549
+ nextCursor,
550
+ hasMore
551
+ };
552
+ }
553
+ /**
554
+ * Get a single video by ID
555
+ */
556
+ async getVideoDetail(id) {
557
+ await this.simulateDelay();
558
+ const video = this.videos.find((v) => v.id === id);
559
+ if (!video) {
560
+ throw new Error(`Video not found: ${id}`);
561
+ }
562
+ return video;
563
+ }
564
+ /**
565
+ * Prefetch videos (no-op for mock)
566
+ */
567
+ async prefetch(_ids) {
568
+ }
569
+ /**
570
+ * Simulate network delay
571
+ */
572
+ async simulateDelay() {
573
+ if (this.delay > 0) {
574
+ await new Promise((resolve) => setTimeout(resolve, this.delay));
575
+ }
576
+ }
577
+ };
578
+
579
+ // src/logger/mock.ts
580
+ var LOG_LEVEL_PRIORITY = {
581
+ debug: 0,
582
+ info: 1,
583
+ warn: 2,
584
+ error: 3
585
+ };
586
+ var MockLoggerAdapter = class {
587
+ constructor(options = {}) {
588
+ this.buffer = [];
589
+ this.maxSize = options.bufferSize ?? 50;
590
+ this.minLevel = options.minLevel ?? "debug";
591
+ this.consoleOutput = options.consoleOutput ?? false;
592
+ }
593
+ /**
594
+ * Log a single entry to the circular buffer
595
+ *
596
+ * @param entry - Log entry to record
597
+ */
598
+ log(entry) {
599
+ if (LOG_LEVEL_PRIORITY[entry.level] < LOG_LEVEL_PRIORITY[this.minLevel]) {
600
+ return;
601
+ }
602
+ this.buffer.push(entry);
603
+ if (this.buffer.length > this.maxSize) {
604
+ this.buffer.shift();
605
+ }
606
+ if (this.consoleOutput) {
607
+ this.outputToConsole(entry);
608
+ }
609
+ }
610
+ /**
611
+ * Flush log entries (called on error for context)
612
+ *
613
+ * @param entries - Array of log entries to flush
614
+ */
615
+ flush(entries) {
616
+ if (this.consoleOutput) {
617
+ for (const entry of entries) {
618
+ this.outputToConsole(entry);
619
+ }
620
+ }
621
+ }
622
+ /**
623
+ * Log debug message
624
+ *
625
+ * @param message - Debug message
626
+ * @param context - Optional context data
627
+ */
628
+ debug(message, context) {
629
+ this.log({
630
+ level: "debug",
631
+ message,
632
+ timestamp: Date.now(),
633
+ context
634
+ });
635
+ }
636
+ /**
637
+ * Log info message
638
+ *
639
+ * @param message - Info message
640
+ * @param context - Optional context data
641
+ */
642
+ info(message, context) {
643
+ this.log({
644
+ level: "info",
645
+ message,
646
+ timestamp: Date.now(),
647
+ context
648
+ });
649
+ }
650
+ /**
651
+ * Log warning message
652
+ *
653
+ * @param message - Warning message
654
+ * @param context - Optional context data
655
+ */
656
+ warn(message, context) {
657
+ this.log({
658
+ level: "warn",
659
+ message,
660
+ timestamp: Date.now(),
661
+ context
662
+ });
663
+ }
664
+ /**
665
+ * Log error message
666
+ *
667
+ * @param message - Error message
668
+ * @param error - Optional Error object
669
+ * @param context - Optional context data
670
+ */
671
+ error(message, error, context) {
672
+ this.log({
673
+ level: "error",
674
+ message,
675
+ timestamp: Date.now(),
676
+ context,
677
+ stack: error?.stack
678
+ });
679
+ }
680
+ // ═══════════════════════════════════════════════════════════════
681
+ // TESTING HELPERS (not part of ILogger interface)
682
+ // ═══════════════════════════════════════════════════════════════
683
+ /**
684
+ * Get current buffer contents (for testing)
685
+ *
686
+ * @returns Copy of current log buffer
687
+ */
688
+ getBuffer() {
689
+ return [...this.buffer];
690
+ }
691
+ /**
692
+ * Get buffer size (for testing)
693
+ *
694
+ * @returns Number of entries in buffer
695
+ */
696
+ getBufferSize() {
697
+ return this.buffer.length;
698
+ }
699
+ /**
700
+ * Clear the buffer (for testing)
701
+ */
702
+ clearBuffer() {
703
+ this.buffer = [];
704
+ }
705
+ /**
706
+ * Get entries by level (for testing)
707
+ *
708
+ * @param level - Log level to filter by
709
+ * @returns Entries matching the level
710
+ */
711
+ getEntriesByLevel(level) {
712
+ return this.buffer.filter((entry) => entry.level === level);
713
+ }
714
+ /**
715
+ * Force flush and return buffer contents
716
+ *
717
+ * @returns All entries in buffer before clearing
718
+ */
719
+ forceFlush() {
720
+ const entries = [...this.buffer];
721
+ this.flush(entries);
722
+ this.buffer = [];
723
+ return entries;
724
+ }
725
+ /**
726
+ * Output entry to console (when consoleOutput is enabled)
727
+ */
728
+ outputToConsole(entry) {
729
+ const timestamp = new Date(entry.timestamp).toISOString();
730
+ const prefix = `[${timestamp}] [${entry.level.toUpperCase()}]`;
731
+ switch (entry.level) {
732
+ case "debug":
733
+ console.debug(prefix, entry.message, entry.context ?? "");
734
+ break;
735
+ case "info":
736
+ console.info(prefix, entry.message, entry.context ?? "");
737
+ break;
738
+ case "warn":
739
+ console.warn(prefix, entry.message, entry.context ?? "");
740
+ break;
741
+ case "error":
742
+ console.error(prefix, entry.message, entry.context ?? "", entry.stack ?? "");
743
+ break;
744
+ }
745
+ }
746
+ };
747
+
748
+ // src/storage/mock.ts
749
+ var MockStorageAdapter = class {
750
+ constructor(options = {}) {
751
+ this.store = /* @__PURE__ */ new Map();
752
+ this.delay = options.delay ?? 0;
753
+ this.errorRate = options.errorRate ?? 0;
754
+ this.simulateQuotaError = options.simulateQuotaError ?? false;
755
+ if (options.initialData) {
756
+ for (const [key, value] of Object.entries(options.initialData)) {
757
+ this.store.set(key, JSON.stringify(value));
758
+ }
759
+ }
760
+ }
761
+ /**
762
+ * Get a value from storage
763
+ *
764
+ * @param key - Storage key
765
+ * @returns Promise resolving to the stored value or null if not found
766
+ */
767
+ async get(key) {
768
+ await this.simulateDelay();
769
+ this.maybeThrowError();
770
+ const value = this.store.get(key);
771
+ if (value === void 0) {
772
+ return null;
773
+ }
774
+ try {
775
+ return JSON.parse(value);
776
+ } catch {
777
+ return null;
778
+ }
779
+ }
780
+ /**
781
+ * Set a value in storage
782
+ *
783
+ * @param key - Storage key
784
+ * @param value - Value to store (will be serialized)
785
+ * @throws DOMException if simulateQuotaError is enabled
786
+ */
787
+ async set(key, value) {
788
+ await this.simulateDelay();
789
+ this.maybeThrowError();
790
+ if (this.simulateQuotaError) {
791
+ throw new DOMException("QuotaExceededError", "QuotaExceededError");
792
+ }
793
+ this.store.set(key, JSON.stringify(value));
794
+ }
795
+ /**
796
+ * Remove a value from storage
797
+ *
798
+ * @param key - Storage key to remove
799
+ */
800
+ async remove(key) {
801
+ await this.simulateDelay();
802
+ this.maybeThrowError();
803
+ this.store.delete(key);
804
+ }
805
+ /**
806
+ * Clear all SDK-related storage
807
+ */
808
+ async clear() {
809
+ await this.simulateDelay();
810
+ this.maybeThrowError();
811
+ this.store.clear();
812
+ }
813
+ /**
814
+ * Get all keys in storage
815
+ *
816
+ * @returns Promise resolving to array of storage keys
817
+ */
818
+ async keys() {
819
+ await this.simulateDelay();
820
+ this.maybeThrowError();
821
+ return Array.from(this.store.keys());
822
+ }
823
+ // ═══════════════════════════════════════════════════════════════
824
+ // TESTING HELPERS (not part of IStorage interface)
825
+ // ═══════════════════════════════════════════════════════════════
826
+ /**
827
+ * Get storage size (for testing)
828
+ *
829
+ * @returns Number of entries in storage
830
+ */
831
+ getSize() {
832
+ return this.store.size;
833
+ }
834
+ /**
835
+ * Check if key exists (for testing)
836
+ *
837
+ * @param key - Storage key to check
838
+ * @returns Whether key exists
839
+ */
840
+ has(key) {
841
+ return this.store.has(key);
842
+ }
843
+ /**
844
+ * Get raw storage Map (for testing)
845
+ *
846
+ * @returns Copy of internal storage map
847
+ */
848
+ getRawStore() {
849
+ return new Map(this.store);
850
+ }
851
+ /**
852
+ * Simulate network/processing delay
853
+ */
854
+ async simulateDelay() {
855
+ if (this.delay > 0) {
856
+ await new Promise((resolve) => setTimeout(resolve, this.delay));
857
+ }
858
+ }
859
+ /**
860
+ * Maybe throw error based on error rate
861
+ */
862
+ maybeThrowError() {
863
+ if (this.errorRate > 0 && Math.random() < this.errorRate) {
864
+ throw new Error("Storage operation failed");
865
+ }
866
+ }
867
+ };
868
+ var SESSION_SNAPSHOT_KEY = "sv-session-snapshot";
869
+ var MockSessionStorageAdapter = class extends MockStorageAdapter {
870
+ constructor(options = {}) {
871
+ super(options);
872
+ this.maxSnapshotAge = options.maxSnapshotAge ?? 24 * 60 * 60 * 1e3;
873
+ }
874
+ /**
875
+ * Save session snapshot for state restoration
876
+ *
877
+ * @param snapshot - Session state to persist
878
+ */
879
+ async saveSnapshot(snapshot) {
880
+ await this.set(SESSION_SNAPSHOT_KEY, snapshot);
881
+ }
882
+ /**
883
+ * Load the most recent session snapshot
884
+ *
885
+ * @returns Promise resolving to snapshot or null if none exists
886
+ */
887
+ async loadSnapshot() {
888
+ const snapshot = await this.get(SESSION_SNAPSHOT_KEY);
889
+ if (!snapshot) {
890
+ return null;
891
+ }
892
+ const age = Date.now() - snapshot.savedAt;
893
+ if (age > this.maxSnapshotAge) {
894
+ await this.clearSnapshot();
895
+ return null;
896
+ }
897
+ return snapshot;
898
+ }
899
+ /**
900
+ * Clear session snapshot
901
+ */
902
+ async clearSnapshot() {
903
+ await this.remove(SESSION_SNAPSHOT_KEY);
904
+ }
905
+ // ═══════════════════════════════════════════════════════════════
906
+ // TESTING HELPERS
907
+ // ═══════════════════════════════════════════════════════════════
908
+ /**
909
+ * Check if snapshot exists (for testing)
910
+ *
911
+ * @returns Whether snapshot exists
912
+ */
913
+ hasSnapshot() {
914
+ return this.has(SESSION_SNAPSHOT_KEY);
915
+ }
916
+ /**
917
+ * Get snapshot age in ms (for testing)
918
+ *
919
+ * @returns Age in ms or null if no snapshot
920
+ */
921
+ async getSnapshotAge() {
922
+ const snapshot = await this.get(SESSION_SNAPSHOT_KEY);
923
+ if (!snapshot) {
924
+ return null;
925
+ }
926
+ return Date.now() - snapshot.savedAt;
927
+ }
928
+ };
929
+
930
+ // src/interaction/mock.ts
931
+ var commentIdCounter = 0;
932
+ var MockInteractionAdapter = class {
933
+ constructor(options = {}) {
934
+ this.likedVideos = /* @__PURE__ */ new Set();
935
+ this.followedAuthors = /* @__PURE__ */ new Set();
936
+ this.likedComments = /* @__PURE__ */ new Set();
937
+ this.comments = /* @__PURE__ */ new Map();
938
+ this.delay = options.delay ?? 0;
939
+ this.errorRate = options.errorRate ?? 0;
940
+ this.failLike = options.failLike ?? false;
941
+ this.failUnlike = options.failUnlike ?? false;
942
+ this.failFollow = options.failFollow ?? false;
943
+ this.failUnfollow = options.failUnfollow ?? false;
944
+ this.failComment = options.failComment ?? false;
945
+ this.failDeleteComment = options.failDeleteComment ?? false;
946
+ if (options.initialLikedVideos) {
947
+ for (const id of options.initialLikedVideos) {
948
+ this.likedVideos.add(id);
949
+ }
950
+ }
951
+ if (options.initialFollowedAuthors) {
952
+ for (const id of options.initialFollowedAuthors) {
953
+ this.followedAuthors.add(id);
954
+ }
955
+ }
956
+ if (options.initialLikedComments) {
957
+ for (const id of options.initialLikedComments) {
958
+ this.likedComments.add(id);
959
+ }
960
+ }
961
+ }
962
+ /**
963
+ * Like a video
964
+ *
965
+ * @param videoId - ID of the video to like
966
+ * @throws Error if failLike is enabled or random error occurs
967
+ */
968
+ async like(videoId) {
969
+ await this.simulateDelay();
970
+ this.maybeThrowError();
971
+ if (this.failLike) {
972
+ throw new Error("Like failed - server error");
973
+ }
974
+ this.likedVideos.add(videoId);
975
+ }
976
+ /**
977
+ * Remove like from a video
978
+ *
979
+ * @param videoId - ID of the video to unlike
980
+ * @throws Error if failUnlike is enabled or random error occurs
981
+ */
982
+ async unlike(videoId) {
983
+ await this.simulateDelay();
984
+ this.maybeThrowError();
985
+ if (this.failUnlike) {
986
+ throw new Error("Unlike failed - server error");
987
+ }
988
+ this.likedVideos.delete(videoId);
989
+ }
990
+ /**
991
+ * Follow a video author
992
+ *
993
+ * @param authorId - ID of the author to follow
994
+ * @throws Error if failFollow is enabled or random error occurs
995
+ */
996
+ async follow(authorId) {
997
+ await this.simulateDelay();
998
+ this.maybeThrowError();
999
+ if (this.failFollow) {
1000
+ throw new Error("Follow failed - server error");
1001
+ }
1002
+ this.followedAuthors.add(authorId);
1003
+ }
1004
+ /**
1005
+ * Unfollow a video author
1006
+ *
1007
+ * @param authorId - ID of the author to unfollow
1008
+ * @throws Error if failUnfollow is enabled or random error occurs
1009
+ */
1010
+ async unfollow(authorId) {
1011
+ await this.simulateDelay();
1012
+ this.maybeThrowError();
1013
+ if (this.failUnfollow) {
1014
+ throw new Error("Unfollow failed - server error");
1015
+ }
1016
+ this.followedAuthors.delete(authorId);
1017
+ }
1018
+ /**
1019
+ * Post a comment on a video
1020
+ *
1021
+ * @param _videoId - ID of the video to comment on (unused in mock)
1022
+ * @param text - Comment text content
1023
+ * @returns Promise resolving to the created comment
1024
+ * @throws Error if failComment is enabled or random error occurs
1025
+ */
1026
+ async comment(_videoId, text) {
1027
+ await this.simulateDelay();
1028
+ this.maybeThrowError();
1029
+ if (this.failComment) {
1030
+ throw new Error("Comment failed - server error");
1031
+ }
1032
+ const newComment = {
1033
+ id: `comment-${++commentIdCounter}`,
1034
+ text,
1035
+ author: {
1036
+ id: "mock-user",
1037
+ name: "Mock User",
1038
+ avatar: "https://i.pravatar.cc/150?u=mock",
1039
+ isVerified: false
1040
+ },
1041
+ likes: 0,
1042
+ isLiked: false,
1043
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1044
+ replyCount: 0
1045
+ };
1046
+ this.comments.set(newComment.id, newComment);
1047
+ return newComment;
1048
+ }
1049
+ /**
1050
+ * Delete a comment
1051
+ *
1052
+ * @param commentId - ID of the comment to delete
1053
+ * @throws Error if comment not found, failDeleteComment is enabled, or random error occurs
1054
+ */
1055
+ async deleteComment(commentId) {
1056
+ await this.simulateDelay();
1057
+ this.maybeThrowError();
1058
+ if (this.failDeleteComment) {
1059
+ throw new Error("Delete comment failed - server error");
1060
+ }
1061
+ if (!this.comments.has(commentId)) {
1062
+ throw new Error(`Comment not found: ${commentId}`);
1063
+ }
1064
+ this.comments.delete(commentId);
1065
+ this.likedComments.delete(commentId);
1066
+ }
1067
+ /**
1068
+ * Like a comment
1069
+ *
1070
+ * @param commentId - ID of the comment to like
1071
+ */
1072
+ async likeComment(commentId) {
1073
+ await this.simulateDelay();
1074
+ this.maybeThrowError();
1075
+ this.likedComments.add(commentId);
1076
+ const comment = this.comments.get(commentId);
1077
+ if (comment) {
1078
+ comment.isLiked = true;
1079
+ comment.likes += 1;
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Unlike a comment
1084
+ *
1085
+ * @param commentId - ID of the comment to unlike
1086
+ */
1087
+ async unlikeComment(commentId) {
1088
+ await this.simulateDelay();
1089
+ this.maybeThrowError();
1090
+ this.likedComments.delete(commentId);
1091
+ const comment = this.comments.get(commentId);
1092
+ if (comment?.isLiked) {
1093
+ comment.isLiked = false;
1094
+ comment.likes = Math.max(0, comment.likes - 1);
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Share a video (tracking only)
1099
+ *
1100
+ * @param _videoId - ID of the video being shared
1101
+ * @param _platform - Optional platform identifier
1102
+ */
1103
+ async share(_videoId, _platform) {
1104
+ await this.simulateDelay();
1105
+ this.maybeThrowError();
1106
+ }
1107
+ /**
1108
+ * Report a video
1109
+ *
1110
+ * @param _videoId - ID of the video to report
1111
+ * @param _reason - Report reason code
1112
+ * @param _description - Optional additional description
1113
+ */
1114
+ async report(_videoId, _reason, _description) {
1115
+ await this.simulateDelay();
1116
+ this.maybeThrowError();
1117
+ }
1118
+ // ═══════════════════════════════════════════════════════════════
1119
+ // TESTING HELPERS (not part of IInteraction interface)
1120
+ // ═══════════════════════════════════════════════════════════════
1121
+ /**
1122
+ * Check if video is liked (for testing)
1123
+ *
1124
+ * @param videoId - Video ID to check
1125
+ * @returns Whether video is liked
1126
+ */
1127
+ isVideoLiked(videoId) {
1128
+ return this.likedVideos.has(videoId);
1129
+ }
1130
+ /**
1131
+ * Check if author is followed (for testing)
1132
+ *
1133
+ * @param authorId - Author ID to check
1134
+ * @returns Whether author is followed
1135
+ */
1136
+ isAuthorFollowed(authorId) {
1137
+ return this.followedAuthors.has(authorId);
1138
+ }
1139
+ /**
1140
+ * Check if comment is liked (for testing)
1141
+ *
1142
+ * @param commentId - Comment ID to check
1143
+ * @returns Whether comment is liked
1144
+ */
1145
+ isCommentLiked(commentId) {
1146
+ return this.likedComments.has(commentId);
1147
+ }
1148
+ /**
1149
+ * Get all liked video IDs (for testing)
1150
+ *
1151
+ * @returns Array of liked video IDs
1152
+ */
1153
+ getLikedVideos() {
1154
+ return Array.from(this.likedVideos);
1155
+ }
1156
+ /**
1157
+ * Get all followed author IDs (for testing)
1158
+ *
1159
+ * @returns Array of followed author IDs
1160
+ */
1161
+ getFollowedAuthors() {
1162
+ return Array.from(this.followedAuthors);
1163
+ }
1164
+ /**
1165
+ * Get all comments (for testing)
1166
+ *
1167
+ * @returns Map of comment ID to Comment
1168
+ */
1169
+ getComments() {
1170
+ return new Map(this.comments);
1171
+ }
1172
+ /**
1173
+ * Get a specific comment (for testing)
1174
+ *
1175
+ * @param commentId - Comment ID
1176
+ * @returns Comment or undefined
1177
+ */
1178
+ getComment(commentId) {
1179
+ return this.comments.get(commentId);
1180
+ }
1181
+ /**
1182
+ * Reset all state (for testing)
1183
+ */
1184
+ reset() {
1185
+ this.likedVideos.clear();
1186
+ this.followedAuthors.clear();
1187
+ this.likedComments.clear();
1188
+ this.comments.clear();
1189
+ }
1190
+ /**
1191
+ * Simulate network/processing delay
1192
+ */
1193
+ async simulateDelay() {
1194
+ if (this.delay > 0) {
1195
+ await new Promise((resolve) => setTimeout(resolve, this.delay));
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Maybe throw error based on error rate
1200
+ */
1201
+ maybeThrowError() {
1202
+ if (this.errorRate > 0 && Math.random() < this.errorRate) {
1203
+ throw new Error("Interaction failed - random error");
1204
+ }
1205
+ }
1206
+ };
1207
+
1208
+ // src/analytics/mock.ts
1209
+ var MockAnalyticsAdapter = class {
1210
+ constructor(options = {}) {
1211
+ this.eventQueue = [];
1212
+ this.flushedEvents = [];
1213
+ this.userId = null;
1214
+ this.userProperties = {};
1215
+ this.batchSize = options.batchSize ?? 10;
1216
+ this.autoFlush = options.autoFlush ?? false;
1217
+ this.flushDelay = options.flushDelay ?? 0;
1218
+ this.errorRate = options.errorRate ?? 0;
1219
+ }
1220
+ /**
1221
+ * Track an analytics event
1222
+ *
1223
+ * @param event - Event to track
1224
+ */
1225
+ track(event) {
1226
+ this.eventQueue.push(event);
1227
+ if (this.autoFlush && this.eventQueue.length >= this.batchSize) {
1228
+ this.flush().catch(() => {
1229
+ });
1230
+ }
1231
+ }
1232
+ /**
1233
+ * Flush queued events immediately
1234
+ *
1235
+ * @returns Promise that resolves when flush is complete
1236
+ */
1237
+ async flush() {
1238
+ if (this.eventQueue.length === 0) {
1239
+ return;
1240
+ }
1241
+ if (this.flushDelay > 0) {
1242
+ await new Promise((resolve) => setTimeout(resolve, this.flushDelay));
1243
+ }
1244
+ if (this.errorRate > 0 && Math.random() < this.errorRate) {
1245
+ return;
1246
+ }
1247
+ const events = [...this.eventQueue];
1248
+ this.flushedEvents.push(events);
1249
+ this.eventQueue = [];
1250
+ }
1251
+ /**
1252
+ * Track video view duration
1253
+ *
1254
+ * @param videoId - ID of the video
1255
+ * @param duration - Watch duration in seconds
1256
+ * @param totalDuration - Total video duration in seconds
1257
+ */
1258
+ trackViewDuration(videoId, duration, totalDuration) {
1259
+ this.track({
1260
+ type: "video_view",
1261
+ videoId,
1262
+ timestamp: Date.now(),
1263
+ data: {
1264
+ duration,
1265
+ totalDuration,
1266
+ percentage: totalDuration > 0 ? duration / totalDuration * 100 : 0
1267
+ }
1268
+ });
1269
+ }
1270
+ /**
1271
+ * Track video completion
1272
+ *
1273
+ * @param videoId - ID of the video
1274
+ * @param watchTime - Total time watched in seconds
1275
+ * @param loops - Number of times video looped
1276
+ */
1277
+ trackCompletion(videoId, watchTime, loops) {
1278
+ this.track({
1279
+ type: "video_complete",
1280
+ videoId,
1281
+ timestamp: Date.now(),
1282
+ data: {
1283
+ watchTime,
1284
+ loops
1285
+ }
1286
+ });
1287
+ }
1288
+ /**
1289
+ * Set user context for analytics
1290
+ *
1291
+ * @param userId - Optional user ID (null for anonymous)
1292
+ * @param properties - Optional user properties
1293
+ */
1294
+ setUser(userId, properties) {
1295
+ this.userId = userId;
1296
+ this.userProperties = properties ?? {};
1297
+ }
1298
+ /**
1299
+ * Get current queue size
1300
+ *
1301
+ * @returns Number of events in queue
1302
+ */
1303
+ getQueueSize() {
1304
+ return this.eventQueue.length;
1305
+ }
1306
+ // ═══════════════════════════════════════════════════════════════
1307
+ // TESTING HELPERS (not part of IAnalytics interface)
1308
+ // ═══════════════════════════════════════════════════════════════
1309
+ /**
1310
+ * Get all tracked events (current queue)
1311
+ *
1312
+ * @returns Copy of current event queue
1313
+ */
1314
+ getTrackedEvents() {
1315
+ return [...this.eventQueue];
1316
+ }
1317
+ /**
1318
+ * Get all flushed event batches
1319
+ *
1320
+ * @returns Array of flushed event batches
1321
+ */
1322
+ getFlushedBatches() {
1323
+ return [...this.flushedEvents];
1324
+ }
1325
+ /**
1326
+ * Get total flushed events count
1327
+ *
1328
+ * @returns Total number of flushed events
1329
+ */
1330
+ getTotalFlushedCount() {
1331
+ return this.flushedEvents.reduce((sum, batch) => sum + batch.length, 0);
1332
+ }
1333
+ /**
1334
+ * Get events by type (from current queue)
1335
+ *
1336
+ * @param type - Event type to filter by
1337
+ * @returns Events matching the type
1338
+ */
1339
+ getEventsByType(type) {
1340
+ return this.eventQueue.filter((event) => event.type === type);
1341
+ }
1342
+ /**
1343
+ * Get events for a specific video (from current queue)
1344
+ *
1345
+ * @param videoId - Video ID to filter by
1346
+ * @returns Events for the video
1347
+ */
1348
+ getEventsByVideoId(videoId) {
1349
+ return this.eventQueue.filter((event) => event.videoId === videoId);
1350
+ }
1351
+ /**
1352
+ * Get current user context
1353
+ *
1354
+ * @returns User ID and properties
1355
+ */
1356
+ getUserContext() {
1357
+ return {
1358
+ userId: this.userId,
1359
+ properties: { ...this.userProperties }
1360
+ };
1361
+ }
1362
+ /**
1363
+ * Check if specific event type was tracked
1364
+ *
1365
+ * @param type - Event type to check
1366
+ * @returns Whether event type was tracked
1367
+ */
1368
+ hasTracked(type) {
1369
+ return this.eventQueue.some((event) => event.type === type);
1370
+ }
1371
+ /**
1372
+ * Get last tracked event
1373
+ *
1374
+ * @returns Last event or undefined
1375
+ */
1376
+ getLastEvent() {
1377
+ return this.eventQueue[this.eventQueue.length - 1];
1378
+ }
1379
+ /**
1380
+ * Reset all state
1381
+ */
1382
+ reset() {
1383
+ this.eventQueue = [];
1384
+ this.flushedEvents = [];
1385
+ this.userId = null;
1386
+ this.userProperties = {};
1387
+ }
1388
+ /**
1389
+ * Clear only the event queue (keep flushed history)
1390
+ */
1391
+ clearQueue() {
1392
+ this.eventQueue = [];
1393
+ }
1394
+ };
1395
+
1396
+ // src/network/mock.ts
1397
+ var MockNetworkAdapter = class {
1398
+ constructor(options = {}) {
1399
+ this.listeners = /* @__PURE__ */ new Set();
1400
+ this.currentType = options.initialType ?? "wifi";
1401
+ this.downlink = options.initialDownlink ?? 10;
1402
+ this.rtt = options.initialRtt ?? 50;
1403
+ this.metered = options.isMetered ?? false;
1404
+ this.online = options.isOnline ?? true;
1405
+ }
1406
+ async getNetworkType() {
1407
+ return this.currentType;
1408
+ }
1409
+ async getNetworkQuality() {
1410
+ return {
1411
+ type: this.currentType,
1412
+ downlink: this.downlink,
1413
+ rtt: this.rtt,
1414
+ isMetered: this.metered
1415
+ };
1416
+ }
1417
+ onNetworkChange(callback) {
1418
+ this.listeners.add(callback);
1419
+ return () => {
1420
+ this.listeners.delete(callback);
1421
+ };
1422
+ }
1423
+ async isOnline() {
1424
+ return this.online;
1425
+ }
1426
+ // ═══════════════════════════════════════════════════════════════
1427
+ // MOCK CONTROL METHODS (for testing)
1428
+ // ═══════════════════════════════════════════════════════════════
1429
+ /**
1430
+ * Set network type and notify listeners
1431
+ */
1432
+ setNetworkType(type) {
1433
+ if (this.currentType !== type) {
1434
+ this.currentType = type;
1435
+ this.notifyListeners();
1436
+ }
1437
+ }
1438
+ /**
1439
+ * Set downlink speed in Mbps
1440
+ */
1441
+ setDownlink(mbps) {
1442
+ this.downlink = mbps;
1443
+ }
1444
+ /**
1445
+ * Set RTT in milliseconds
1446
+ */
1447
+ setRtt(ms) {
1448
+ this.rtt = ms;
1449
+ }
1450
+ /**
1451
+ * Set metered status (e.g., cellular data)
1452
+ */
1453
+ setMetered(metered) {
1454
+ this.metered = metered;
1455
+ }
1456
+ /**
1457
+ * Set online/offline status
1458
+ */
1459
+ setOnline(online) {
1460
+ this.online = online;
1461
+ if (!online) {
1462
+ this.currentType = "offline";
1463
+ this.notifyListeners();
1464
+ }
1465
+ }
1466
+ /**
1467
+ * Simulate network change sequence for testing
1468
+ */
1469
+ async simulateNetworkChanges(sequence, delayMs = 100) {
1470
+ for (const type of sequence) {
1471
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1472
+ this.setNetworkType(type);
1473
+ }
1474
+ }
1475
+ /**
1476
+ * Reset to default state
1477
+ */
1478
+ reset() {
1479
+ this.currentType = "wifi";
1480
+ this.downlink = 10;
1481
+ this.rtt = 50;
1482
+ this.metered = false;
1483
+ this.online = true;
1484
+ this.listeners.clear();
1485
+ }
1486
+ notifyListeners() {
1487
+ for (const listener of this.listeners) {
1488
+ listener(this.currentType);
1489
+ }
1490
+ }
1491
+ };
1492
+
1493
+ // src/video-loader/mock.ts
1494
+ var MockVideoLoader = class {
1495
+ constructor(options = {}) {
1496
+ this.preloads = /* @__PURE__ */ new Map();
1497
+ this.preloadDelayMs = options.preloadDelayMs ?? 100;
1498
+ this.shouldSucceed = options.shouldSucceed ?? true;
1499
+ this.errorMessage = options.errorMessage ?? "Mock preload failed";
1500
+ this.bytesPerPreload = options.bytesPerPreload ?? 5e5;
1501
+ }
1502
+ // ═══════════════════════════════════════════════════════════════
1503
+ // IVideoLoader Implementation
1504
+ // ═══════════════════════════════════════════════════════════════
1505
+ async preload(_videoId, _source, _config) {
1506
+ const abortController = new AbortController();
1507
+ this.preloads.set(_videoId, {
1508
+ status: "loading",
1509
+ loadedBytes: 0,
1510
+ abortController
1511
+ });
1512
+ try {
1513
+ await new Promise((resolve, reject) => {
1514
+ const timeoutId = setTimeout(resolve, this.preloadDelayMs);
1515
+ abortController.signal.addEventListener("abort", () => {
1516
+ clearTimeout(timeoutId);
1517
+ reject(new Error("Preload cancelled"));
1518
+ });
1519
+ });
1520
+ if (abortController.signal.aborted) {
1521
+ return {
1522
+ videoId: _videoId,
1523
+ status: "idle",
1524
+ loadedBytes: 0
1525
+ };
1526
+ }
1527
+ if (this.shouldSucceed) {
1528
+ this.preloads.set(_videoId, {
1529
+ status: "ready",
1530
+ loadedBytes: this.bytesPerPreload
1531
+ });
1532
+ return {
1533
+ videoId: _videoId,
1534
+ status: "ready",
1535
+ loadedBytes: this.bytesPerPreload
1536
+ };
1537
+ }
1538
+ const error = new Error(this.errorMessage);
1539
+ this.preloads.set(_videoId, {
1540
+ status: "error",
1541
+ loadedBytes: 0,
1542
+ error
1543
+ });
1544
+ return {
1545
+ videoId: _videoId,
1546
+ status: "error",
1547
+ error
1548
+ };
1549
+ } catch (e) {
1550
+ if (e instanceof Error && e.message === "Preload cancelled") {
1551
+ this.preloads.set(_videoId, {
1552
+ status: "idle",
1553
+ loadedBytes: 0
1554
+ });
1555
+ return {
1556
+ videoId: _videoId,
1557
+ status: "idle",
1558
+ loadedBytes: 0
1559
+ };
1560
+ }
1561
+ throw e;
1562
+ }
1563
+ }
1564
+ cancelPreload(videoId) {
1565
+ const state = this.preloads.get(videoId);
1566
+ if (state?.abortController) {
1567
+ state.abortController.abort();
1568
+ }
1569
+ this.preloads.set(videoId, {
1570
+ status: "idle",
1571
+ loadedBytes: 0
1572
+ });
1573
+ }
1574
+ isPreloaded(videoId) {
1575
+ return this.preloads.get(videoId)?.status === "ready";
1576
+ }
1577
+ getPreloadStatus(videoId) {
1578
+ return this.preloads.get(videoId)?.status ?? "idle";
1579
+ }
1580
+ clearPreload(videoId) {
1581
+ this.preloads.delete(videoId);
1582
+ }
1583
+ clearAll() {
1584
+ this.preloads.clear();
1585
+ }
1586
+ getTotalPreloadedBytes() {
1587
+ let total = 0;
1588
+ for (const state of this.preloads.values()) {
1589
+ if (state.status === "ready") {
1590
+ total += state.loadedBytes;
1591
+ }
1592
+ }
1593
+ return total;
1594
+ }
1595
+ // ═══════════════════════════════════════════════════════════════
1596
+ // MOCK CONTROL METHODS (for testing)
1597
+ // ═══════════════════════════════════════════════════════════════
1598
+ /**
1599
+ * Set whether preloads should succeed
1600
+ */
1601
+ setShouldSucceed(shouldSucceed) {
1602
+ this.shouldSucceed = shouldSucceed;
1603
+ }
1604
+ /**
1605
+ * Set preload delay in milliseconds
1606
+ */
1607
+ setPreloadDelay(ms) {
1608
+ this.preloadDelayMs = ms;
1609
+ }
1610
+ /**
1611
+ * Set error message for failed preloads
1612
+ */
1613
+ setErrorMessage(message) {
1614
+ this.errorMessage = message;
1615
+ }
1616
+ /**
1617
+ * Get all tracked preloads (for testing assertions)
1618
+ */
1619
+ getPreloads() {
1620
+ return new Map(this.preloads);
1621
+ }
1622
+ /**
1623
+ * Reset all state
1624
+ */
1625
+ reset() {
1626
+ this.preloads.clear();
1627
+ this.shouldSucceed = true;
1628
+ this.preloadDelayMs = 100;
1629
+ this.bytesPerPreload = 5e5;
1630
+ this.errorMessage = "Mock preload failed";
1631
+ }
1632
+ };
1633
+ var MockPosterLoader = class {
1634
+ constructor(preloadDelayMs = 50) {
1635
+ this.posterCache = /* @__PURE__ */ new Set();
1636
+ this.preloadDelayMs = preloadDelayMs;
1637
+ }
1638
+ async preload(url) {
1639
+ await new Promise((resolve) => setTimeout(resolve, this.preloadDelayMs));
1640
+ this.posterCache.add(url);
1641
+ }
1642
+ isCached(url) {
1643
+ return this.posterCache.has(url);
1644
+ }
1645
+ clearCache() {
1646
+ this.posterCache.clear();
1647
+ }
1648
+ /**
1649
+ * Set preload delay in milliseconds
1650
+ */
1651
+ setPreloadDelay(ms) {
1652
+ this.preloadDelayMs = ms;
1653
+ }
1654
+ /**
1655
+ * Reset all state
1656
+ */
1657
+ reset() {
1658
+ this.posterCache.clear();
1659
+ this.preloadDelayMs = 50;
1660
+ }
1661
+ };
1662
+
1663
+ // src/preset/adapters/RESTAnalyticsAdapter.ts
1664
+ var DEFAULT_BATCH_SIZE = 10;
1665
+ var DEFAULT_FLUSH_INTERVAL = 3e4;
1666
+ var RESTAnalyticsAdapter = class {
1667
+ constructor(config) {
1668
+ // Event queue
1669
+ this.queue = [];
1670
+ // Flush interval timer
1671
+ this.flushTimer = null;
1672
+ // User context
1673
+ this.userId = null;
1674
+ this.userProperties = {};
1675
+ this.httpClient = config.httpClient;
1676
+ this.endpoint = config.endpoints.batch;
1677
+ this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
1678
+ this.logger = config.logger;
1679
+ const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
1680
+ this.flushTimer = setInterval(() => this.flush(), interval);
1681
+ }
1682
+ /**
1683
+ * Track an analytics event
1684
+ * Fire-and-forget - doesn't throw
1685
+ */
1686
+ track(event) {
1687
+ try {
1688
+ const enrichedEvent = {
1689
+ ...event,
1690
+ userId: this.userId,
1691
+ userProperties: this.userProperties
1692
+ };
1693
+ this.queue.push(enrichedEvent);
1694
+ if (this.queue.length >= this.batchSize) {
1695
+ this.flush().catch(() => {
1696
+ });
1697
+ }
1698
+ } catch (error) {
1699
+ this.logger?.warn("[RESTAnalyticsAdapter] Failed to track event", { error });
1700
+ }
1701
+ }
1702
+ /**
1703
+ * Flush queued events
1704
+ */
1705
+ async flush() {
1706
+ if (this.queue.length === 0) {
1707
+ return;
1708
+ }
1709
+ const events = [...this.queue];
1710
+ this.queue = [];
1711
+ try {
1712
+ if (this.trySendBeacon(events)) {
1713
+ this.logger?.debug("[RESTAnalyticsAdapter] Events sent via sendBeacon", {
1714
+ count: events.length
1715
+ });
1716
+ return;
1717
+ }
1718
+ await this.httpClient.request({
1719
+ method: "POST",
1720
+ path: this.endpoint,
1721
+ body: { events }
1722
+ });
1723
+ this.logger?.debug("[RESTAnalyticsAdapter] Events sent via HTTP", {
1724
+ count: events.length
1725
+ });
1726
+ } catch (error) {
1727
+ if (this.queue.length < this.batchSize * 3) {
1728
+ this.queue = [...events, ...this.queue];
1729
+ }
1730
+ this.logger?.warn("[RESTAnalyticsAdapter] Failed to flush events", { error });
1731
+ }
1732
+ }
1733
+ /**
1734
+ * Track video view duration (heartbeat)
1735
+ */
1736
+ trackViewDuration(videoId, duration, totalDuration) {
1737
+ this.track({
1738
+ type: "video_view",
1739
+ videoId,
1740
+ timestamp: Date.now(),
1741
+ data: {
1742
+ watchTime: duration,
1743
+ totalDuration,
1744
+ percentage: totalDuration > 0 ? Math.round(duration / totalDuration * 100) : 0
1745
+ }
1746
+ });
1747
+ }
1748
+ /**
1749
+ * Track video completion
1750
+ */
1751
+ trackCompletion(videoId, watchTime, loops) {
1752
+ this.track({
1753
+ type: "video_complete",
1754
+ videoId,
1755
+ timestamp: Date.now(),
1756
+ data: {
1757
+ watchTime,
1758
+ loops
1759
+ }
1760
+ });
1761
+ }
1762
+ /**
1763
+ * Set user context
1764
+ */
1765
+ setUser(userId, properties) {
1766
+ this.userId = userId;
1767
+ this.userProperties = properties ?? {};
1768
+ }
1769
+ /**
1770
+ * Get current queue size (for debugging)
1771
+ */
1772
+ getQueueSize() {
1773
+ return this.queue.length;
1774
+ }
1775
+ /**
1776
+ * Cleanup - stop flush interval
1777
+ */
1778
+ destroy() {
1779
+ if (this.flushTimer) {
1780
+ clearInterval(this.flushTimer);
1781
+ this.flushTimer = null;
1782
+ }
1783
+ this.flush().catch(() => {
1784
+ });
1785
+ }
1786
+ /**
1787
+ * Try to send via sendBeacon (for reliability on page unload)
1788
+ */
1789
+ trySendBeacon(events) {
1790
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) {
1791
+ return false;
1792
+ }
1793
+ try {
1794
+ const url = this.buildFullUrl();
1795
+ const data = JSON.stringify({ events });
1796
+ const blob = new Blob([data], { type: "application/json" });
1797
+ return navigator.sendBeacon(url, blob);
1798
+ } catch {
1799
+ return false;
1800
+ }
1801
+ }
1802
+ /**
1803
+ * Build full URL for sendBeacon
1804
+ */
1805
+ buildFullUrl() {
1806
+ return this.endpoint.startsWith("http") ? this.endpoint : `${window.location.origin}${this.endpoint}`;
1807
+ }
1808
+ };
1809
+ function createNoOpAnalyticsAdapter() {
1810
+ return {
1811
+ track: () => {
1812
+ },
1813
+ flush: async () => {
1814
+ },
1815
+ trackViewDuration: () => {
1816
+ },
1817
+ trackCompletion: () => {
1818
+ },
1819
+ setUser: () => {
1820
+ },
1821
+ getQueueSize: () => 0
1822
+ };
1823
+ }
1824
+
1825
+ // src/preset/adapters/RESTDataAdapter.ts
1826
+ var RESTDataAdapter = class {
1827
+ constructor(config) {
1828
+ this.httpClient = config.httpClient;
1829
+ this.endpoints = config.endpoints;
1830
+ this.transforms = config.transforms;
1831
+ this.pagination = config.pagination ?? { cursor: "cursor", limit: "limit" };
1832
+ this.logger = config.logger;
1833
+ }
1834
+ /**
1835
+ * Fetch feed with pagination
1836
+ */
1837
+ async fetchFeed(cursor) {
1838
+ try {
1839
+ const params = {
1840
+ [this.pagination.cursor]: cursor ?? null,
1841
+ [this.pagination.limit]: 10
1842
+ };
1843
+ const response = await this.httpClient.request({
1844
+ method: "GET",
1845
+ path: this.endpoints.list,
1846
+ params
1847
+ });
1848
+ const feedData = this.transforms.feedResponse(response);
1849
+ const items = feedData.items.map((item) => {
1850
+ try {
1851
+ return this.transforms.videoItem(item);
1852
+ } catch (error) {
1853
+ this.logger?.error("[RESTDataAdapter] Failed to transform video item", error);
1854
+ return this.createFallbackVideoItem(item);
1855
+ }
1856
+ });
1857
+ return {
1858
+ items,
1859
+ nextCursor: feedData.nextCursor,
1860
+ hasMore: feedData.hasMore
1861
+ };
1862
+ } catch (error) {
1863
+ this.logger?.error("[RESTDataAdapter] fetchFeed failed", error);
1864
+ throw error;
1865
+ }
1866
+ }
1867
+ /**
1868
+ * Get video detail by ID
1869
+ */
1870
+ async getVideoDetail(id) {
1871
+ try {
1872
+ const response = await this.httpClient.request({
1873
+ method: "GET",
1874
+ path: this.endpoints.detail,
1875
+ pathParams: { id }
1876
+ });
1877
+ const videoData = this.unwrapResponse(response);
1878
+ return this.transforms.videoItem(videoData);
1879
+ } catch (error) {
1880
+ this.logger?.error("[RESTDataAdapter] getVideoDetail failed", error);
1881
+ throw error;
1882
+ }
1883
+ }
1884
+ /**
1885
+ * Optional: Prefetch videos
1886
+ * This is a no-op by default, can be overridden if API supports batch fetch
1887
+ */
1888
+ async prefetch(ids) {
1889
+ this.logger?.debug("[RESTDataAdapter] prefetch called", { ids });
1890
+ }
1891
+ /**
1892
+ * Unwrap response if wrapped in data/result field
1893
+ */
1894
+ unwrapResponse(response) {
1895
+ if (!response || typeof response !== "object") {
1896
+ return response;
1897
+ }
1898
+ const obj = response;
1899
+ if (obj.data !== void 0) return obj.data;
1900
+ if (obj.result !== void 0) return obj.result;
1901
+ if (obj.video !== void 0) return obj.video;
1902
+ if (obj.item !== void 0) return obj.item;
1903
+ return response;
1904
+ }
1905
+ /**
1906
+ * Create fallback video item when transform fails
1907
+ */
1908
+ createFallbackVideoItem(data) {
1909
+ const obj = data ?? {};
1910
+ return {
1911
+ id: String(obj.id ?? obj.video_id ?? `fallback-${Date.now()}`),
1912
+ source: {
1913
+ url: String(obj.video_url ?? obj.url ?? ""),
1914
+ type: "mp4"
1915
+ },
1916
+ duration: 0,
1917
+ author: {
1918
+ id: "unknown",
1919
+ name: "Unknown"
1920
+ },
1921
+ stats: {
1922
+ views: 0,
1923
+ likes: 0,
1924
+ comments: 0,
1925
+ shares: 0
1926
+ },
1927
+ isLiked: false,
1928
+ isFollowing: false,
1929
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1930
+ };
1931
+ }
1932
+ };
1933
+
1934
+ // src/preset/adapters/RESTInteractionAdapter.ts
1935
+ var RESTInteractionAdapter = class {
1936
+ constructor(config) {
1937
+ this.httpClient = config.httpClient;
1938
+ this.endpoints = config.endpoints;
1939
+ this.logger = config.logger;
1940
+ }
1941
+ /**
1942
+ * Like a video
1943
+ */
1944
+ async like(videoId) {
1945
+ try {
1946
+ await this.httpClient.request({
1947
+ method: "POST",
1948
+ path: this.endpoints.like,
1949
+ pathParams: { id: videoId }
1950
+ });
1951
+ } catch (error) {
1952
+ this.logger?.error("[RESTInteractionAdapter] like failed", error);
1953
+ throw error;
1954
+ }
1955
+ }
1956
+ /**
1957
+ * Unlike a video
1958
+ */
1959
+ async unlike(videoId) {
1960
+ try {
1961
+ await this.httpClient.request({
1962
+ method: "DELETE",
1963
+ path: this.endpoints.unlike,
1964
+ pathParams: { id: videoId }
1965
+ });
1966
+ } catch (error) {
1967
+ this.logger?.error("[RESTInteractionAdapter] unlike failed", error);
1968
+ throw error;
1969
+ }
1970
+ }
1971
+ /**
1972
+ * Follow an author
1973
+ */
1974
+ async follow(authorId) {
1975
+ try {
1976
+ await this.httpClient.request({
1977
+ method: "POST",
1978
+ path: this.endpoints.follow,
1979
+ pathParams: { id: authorId }
1980
+ });
1981
+ } catch (error) {
1982
+ this.logger?.error("[RESTInteractionAdapter] follow failed", error);
1983
+ throw error;
1984
+ }
1985
+ }
1986
+ /**
1987
+ * Unfollow an author
1988
+ */
1989
+ async unfollow(authorId) {
1990
+ try {
1991
+ await this.httpClient.request({
1992
+ method: "DELETE",
1993
+ path: this.endpoints.unfollow,
1994
+ pathParams: { id: authorId }
1995
+ });
1996
+ } catch (error) {
1997
+ this.logger?.error("[RESTInteractionAdapter] unfollow failed", error);
1998
+ throw error;
1999
+ }
2000
+ }
2001
+ /**
2002
+ * Post a comment
2003
+ */
2004
+ async comment(videoId, text) {
2005
+ try {
2006
+ const response = await this.httpClient.request({
2007
+ method: "POST",
2008
+ path: this.endpoints.comment,
2009
+ pathParams: { id: videoId },
2010
+ body: { text }
2011
+ });
2012
+ return this.transformComment(response);
2013
+ } catch (error) {
2014
+ this.logger?.error("[RESTInteractionAdapter] comment failed", error);
2015
+ throw error;
2016
+ }
2017
+ }
2018
+ /**
2019
+ * Delete a comment
2020
+ */
2021
+ async deleteComment(commentId) {
2022
+ try {
2023
+ await this.httpClient.request({
2024
+ method: "DELETE",
2025
+ path: this.endpoints.deleteComment,
2026
+ pathParams: { id: commentId }
2027
+ });
2028
+ } catch (error) {
2029
+ this.logger?.error("[RESTInteractionAdapter] deleteComment failed", error);
2030
+ throw error;
2031
+ }
2032
+ }
2033
+ /**
2034
+ * Like a comment
2035
+ */
2036
+ async likeComment(commentId) {
2037
+ this.logger?.debug("[RESTInteractionAdapter] likeComment not implemented", { commentId });
2038
+ }
2039
+ /**
2040
+ * Unlike a comment
2041
+ */
2042
+ async unlikeComment(commentId) {
2043
+ this.logger?.debug("[RESTInteractionAdapter] unlikeComment not implemented", { commentId });
2044
+ }
2045
+ /**
2046
+ * Share a video (optional tracking)
2047
+ */
2048
+ async share(videoId, platform) {
2049
+ if (!this.endpoints.share) {
2050
+ this.logger?.debug("[RESTInteractionAdapter] share endpoint not configured");
2051
+ return;
2052
+ }
2053
+ try {
2054
+ await this.httpClient.request({
2055
+ method: "POST",
2056
+ path: this.endpoints.share,
2057
+ pathParams: { id: videoId },
2058
+ body: platform ? { platform } : void 0
2059
+ });
2060
+ } catch (error) {
2061
+ this.logger?.warn("[RESTInteractionAdapter] share tracking failed", { error });
2062
+ }
2063
+ }
2064
+ /**
2065
+ * Transform API comment response to Comment type
2066
+ */
2067
+ transformComment(response) {
2068
+ const obj = this.unwrapResponse(response);
2069
+ const user = obj.user ?? {};
2070
+ const author = obj.author ?? {};
2071
+ return {
2072
+ id: String(obj.id ?? obj.comment_id ?? ""),
2073
+ text: String(obj.text ?? obj.content ?? obj.body ?? ""),
2074
+ author: {
2075
+ id: String(user.id ?? author.id ?? obj.user_id ?? ""),
2076
+ name: String(user.display_name ?? user.name ?? author.name ?? "Unknown"),
2077
+ avatar: user.avatar ?? author.avatar
2078
+ },
2079
+ likes: Number(obj.like_count ?? obj.likes ?? 0),
2080
+ isLiked: Boolean(obj.is_liked ?? obj.liked ?? false),
2081
+ createdAt: String(obj.created_at ?? obj.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()),
2082
+ replyCount: Number(obj.reply_count ?? obj.replies ?? 0)
2083
+ };
2084
+ }
2085
+ /**
2086
+ * Unwrap response if wrapped
2087
+ */
2088
+ unwrapResponse(response) {
2089
+ if (!response || typeof response !== "object") return response;
2090
+ const obj = response;
2091
+ if (obj.data !== void 0) return obj.data;
2092
+ if (obj.comment !== void 0) return obj.comment;
2093
+ if (obj.result !== void 0) return obj.result;
2094
+ return response;
2095
+ }
2096
+ };
2097
+
2098
+ // src/preset/types.ts
2099
+ var DEFAULT_RETRY_CONFIG = {
2100
+ maxRetries: 3,
2101
+ retryDelay: 1e3,
2102
+ retryOn: [408, 429, 500, 502, 503, 504],
2103
+ exponentialBackoff: true
2104
+ };
2105
+ var DEFAULT_REQUEST_CONFIG = {
2106
+ defaultParams: {},
2107
+ timeout: 1e4,
2108
+ retry: DEFAULT_RETRY_CONFIG,
2109
+ pagination: {
2110
+ cursor: "cursor",
2111
+ limit: "limit"
2112
+ }
2113
+ };
2114
+
2115
+ // src/preset/http/client.ts
2116
+ var RetryableError = class extends Error {
2117
+ constructor(status, message) {
2118
+ super(message);
2119
+ this.status = status;
2120
+ this.name = "RetryableError";
2121
+ }
2122
+ };
2123
+ var HttpError = class extends Error {
2124
+ constructor(status, message, body) {
2125
+ super(message);
2126
+ this.status = status;
2127
+ this.body = body;
2128
+ this.name = "HttpError";
2129
+ }
2130
+ };
2131
+ var HttpClient = class {
2132
+ constructor(config) {
2133
+ // Token refresh deduplication
2134
+ this.isRefreshing = false;
2135
+ this.refreshPromise = null;
2136
+ this.config = config;
2137
+ const mergedRetry = {
2138
+ maxRetries: config.request?.retry?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
2139
+ retryDelay: config.request?.retry?.retryDelay ?? DEFAULT_RETRY_CONFIG.retryDelay,
2140
+ retryOn: config.request?.retry?.retryOn ?? DEFAULT_RETRY_CONFIG.retryOn,
2141
+ exponentialBackoff: config.request?.retry?.exponentialBackoff ?? DEFAULT_RETRY_CONFIG.exponentialBackoff
2142
+ };
2143
+ this.requestConfig = {
2144
+ defaultParams: config.request?.defaultParams ?? DEFAULT_REQUEST_CONFIG.defaultParams,
2145
+ timeout: config.request?.timeout ?? DEFAULT_REQUEST_CONFIG.timeout,
2146
+ retry: mergedRetry,
2147
+ pagination: {
2148
+ cursor: config.request?.pagination?.cursor ?? DEFAULT_REQUEST_CONFIG.pagination.cursor,
2149
+ limit: config.request?.pagination?.limit ?? DEFAULT_REQUEST_CONFIG.pagination.limit
2150
+ }
2151
+ };
2152
+ this.retryConfig = mergedRetry;
2153
+ }
2154
+ /**
2155
+ * Make HTTP request with auth and retry
2156
+ */
2157
+ async request(options) {
2158
+ const { method, path, params, body, pathParams } = options;
2159
+ let url = this.buildUrl(path, pathParams);
2160
+ if (params) {
2161
+ url = this.appendQueryParams(url, params);
2162
+ }
2163
+ const headers = await this.buildHeaders();
2164
+ return this.executeWithRetry({ url, method, headers, body });
2165
+ }
2166
+ /**
2167
+ * Build full URL from path and path params
2168
+ */
2169
+ buildUrl(path, pathParams) {
2170
+ let finalPath = path;
2171
+ if (pathParams) {
2172
+ for (const [key, value] of Object.entries(pathParams)) {
2173
+ finalPath = finalPath.replace(`:${key}`, encodeURIComponent(value));
2174
+ }
2175
+ }
2176
+ return `${this.config.baseUrl}${finalPath}`;
2177
+ }
2178
+ /**
2179
+ * Append query params to URL
2180
+ */
2181
+ appendQueryParams(url, params) {
2182
+ const searchParams = new URLSearchParams();
2183
+ for (const [key, value] of Object.entries(this.requestConfig.defaultParams)) {
2184
+ searchParams.set(key, value);
2185
+ }
2186
+ for (const [key, value] of Object.entries(params)) {
2187
+ if (value != null) {
2188
+ searchParams.set(key, String(value));
2189
+ }
2190
+ }
2191
+ const queryString = searchParams.toString();
2192
+ return queryString ? `${url}?${queryString}` : url;
2193
+ }
2194
+ /**
2195
+ * Build headers with auth token
2196
+ */
2197
+ async buildHeaders() {
2198
+ const headers = {
2199
+ "Content-Type": "application/json",
2200
+ Accept: "application/json",
2201
+ ...this.config.auth.headers
2202
+ };
2203
+ const token = await this.config.auth.getAccessToken();
2204
+ if (token) {
2205
+ const headerName = this.config.auth.tokenHeader ?? "Authorization";
2206
+ const prefix = this.config.auth.tokenPrefix ?? "Bearer";
2207
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
2208
+ }
2209
+ return headers;
2210
+ }
2211
+ /**
2212
+ * Execute request with retry logic
2213
+ */
2214
+ async executeWithRetry(request) {
2215
+ let lastError = null;
2216
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
2217
+ try {
2218
+ const response = await this.doFetch(request);
2219
+ return await this.handleResponse(response, request);
2220
+ } catch (error) {
2221
+ lastError = error;
2222
+ if (this.shouldRetry(error, attempt)) {
2223
+ await this.waitBeforeRetry(attempt, request.url, error);
2224
+ continue;
2225
+ }
2226
+ throw error;
2227
+ }
2228
+ }
2229
+ throw lastError ?? new Error("Request failed");
2230
+ }
2231
+ /**
2232
+ * Handle response status and parse result
2233
+ */
2234
+ async handleResponse(response, request) {
2235
+ if (response.status === 401) {
2236
+ return this.handleUnauthorized(request);
2237
+ }
2238
+ if (this.retryConfig.retryOn.includes(response.status)) {
2239
+ const errorText = await response.text();
2240
+ throw new RetryableError(response.status, errorText);
2241
+ }
2242
+ if (!response.ok) {
2243
+ const errorBody = await this.safeParseJson(response);
2244
+ throw new HttpError(
2245
+ response.status,
2246
+ `Request failed with status ${response.status}`,
2247
+ errorBody
2248
+ );
2249
+ }
2250
+ return this.parseResponse(response);
2251
+ }
2252
+ /**
2253
+ * Handle 401 unauthorized - attempt token refresh
2254
+ */
2255
+ async handleUnauthorized(request) {
2256
+ const newToken = await this.refreshTokenIfNeeded();
2257
+ if (newToken) {
2258
+ this.updateAuthHeader(request.headers, newToken);
2259
+ const retryResponse = await this.doFetch(request);
2260
+ return this.parseResponse(retryResponse);
2261
+ }
2262
+ this.config.auth.onAuthError?.({ status: 401, message: "Authentication failed" });
2263
+ throw new HttpError(401, "Authentication failed");
2264
+ }
2265
+ /**
2266
+ * Update auth header with new token
2267
+ */
2268
+ updateAuthHeader(headers, token) {
2269
+ const headerName = this.config.auth.tokenHeader ?? "Authorization";
2270
+ const prefix = this.config.auth.tokenPrefix ?? "Bearer";
2271
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
2272
+ }
2273
+ /**
2274
+ * Check if error should trigger retry
2275
+ */
2276
+ shouldRetry(error, attempt) {
2277
+ return error instanceof RetryableError && attempt < this.retryConfig.maxRetries;
2278
+ }
2279
+ /**
2280
+ * Wait before retry with backoff
2281
+ */
2282
+ async waitBeforeRetry(attempt, url, error) {
2283
+ const delay = this.retryConfig.exponentialBackoff ? this.retryConfig.retryDelay * 2 ** attempt : this.retryConfig.retryDelay;
2284
+ this.config.logger?.warn(
2285
+ `[HttpClient] Retrying (${attempt + 1}/${this.retryConfig.maxRetries}) after ${delay}ms`,
2286
+ { url, status: error.status }
2287
+ );
2288
+ await this.sleep(delay);
2289
+ }
2290
+ /**
2291
+ * Execute fetch with timeout
2292
+ */
2293
+ async doFetch(request) {
2294
+ const controller = new AbortController();
2295
+ const timeoutId = setTimeout(() => controller.abort(), this.requestConfig.timeout);
2296
+ try {
2297
+ return await fetch(request.url, {
2298
+ method: request.method,
2299
+ headers: request.headers,
2300
+ body: request.body ? JSON.stringify(request.body) : void 0,
2301
+ signal: controller.signal
2302
+ });
2303
+ } catch (error) {
2304
+ if (error instanceof Error && error.name === "AbortError") {
2305
+ throw new HttpError(408, "Request timeout");
2306
+ }
2307
+ throw error;
2308
+ } finally {
2309
+ clearTimeout(timeoutId);
2310
+ }
2311
+ }
2312
+ /**
2313
+ * Parse response JSON
2314
+ */
2315
+ async parseResponse(response) {
2316
+ const text = await response.text();
2317
+ if (!text) {
2318
+ return {};
2319
+ }
2320
+ try {
2321
+ return JSON.parse(text);
2322
+ } catch {
2323
+ return { data: text };
2324
+ }
2325
+ }
2326
+ /**
2327
+ * Safely parse JSON from response
2328
+ */
2329
+ async safeParseJson(response) {
2330
+ try {
2331
+ return await response.json();
2332
+ } catch {
2333
+ return null;
2334
+ }
2335
+ }
2336
+ /**
2337
+ * Refresh token with deduplication
2338
+ */
2339
+ async refreshTokenIfNeeded() {
2340
+ if (!this.config.auth.refreshToken) {
2341
+ return null;
2342
+ }
2343
+ if (this.isRefreshing && this.refreshPromise) {
2344
+ return this.refreshPromise;
2345
+ }
2346
+ this.isRefreshing = true;
2347
+ this.refreshPromise = this.config.auth.refreshToken().finally(() => {
2348
+ this.isRefreshing = false;
2349
+ this.refreshPromise = null;
2350
+ });
2351
+ try {
2352
+ return await this.refreshPromise;
2353
+ } catch (error) {
2354
+ this.config.logger?.error("[HttpClient] Token refresh failed", error);
2355
+ return null;
2356
+ }
2357
+ }
2358
+ /**
2359
+ * Sleep utility
2360
+ */
2361
+ sleep(ms) {
2362
+ return new Promise((resolve) => setTimeout(resolve, ms));
2363
+ }
2364
+ };
2365
+
2366
+ // src/preset/transforms/defaults.ts
2367
+ function getNestedValue(obj, path) {
2368
+ if (!obj || typeof obj !== "object") return void 0;
2369
+ const keys = path.split(".");
2370
+ let current = obj;
2371
+ for (const key of keys) {
2372
+ if (current === null || current === void 0) return void 0;
2373
+ if (typeof current !== "object") return void 0;
2374
+ current = current[key];
2375
+ }
2376
+ return current;
2377
+ }
2378
+ function tryFields(obj, ...paths) {
2379
+ for (const path of paths) {
2380
+ const value = getNestedValue(obj, path);
2381
+ if (value !== void 0 && value !== null) {
2382
+ return value;
2383
+ }
2384
+ }
2385
+ return void 0;
2386
+ }
2387
+ function toSafeString(value, fallback = "") {
2388
+ if (value === null || value === void 0) return fallback ?? "";
2389
+ return String(value);
2390
+ }
2391
+ function toStringOrNull(value) {
2392
+ if (value === null || value === void 0 || value === "") return null;
2393
+ return String(value);
2394
+ }
2395
+ function toNumber(value, fallback = 0) {
2396
+ if (value === null || value === void 0) return fallback;
2397
+ const num = Number(value);
2398
+ return Number.isNaN(num) ? fallback : num;
2399
+ }
2400
+ function toBoolean(value, fallback = false) {
2401
+ if (value === null || value === void 0) return fallback;
2402
+ if (typeof value === "boolean") return value;
2403
+ if (typeof value === "string") {
2404
+ return value.toLowerCase() === "true" || value === "1";
2405
+ }
2406
+ return Boolean(value);
2407
+ }
2408
+ function defaultSourceTransform(data) {
2409
+ const obj = data;
2410
+ const url = toSafeString(
2411
+ tryFields(obj, "video_url", "url", "source_url", "playback_url", "stream_url"),
2412
+ ""
2413
+ );
2414
+ const typeRaw = toSafeString(
2415
+ tryFields(obj, "video_type", "type", "source_type", "format"),
2416
+ "mp4"
2417
+ );
2418
+ const type = typeRaw.toLowerCase().includes("hls") ? "hls" : "mp4";
2419
+ return { url, type };
2420
+ }
2421
+ function defaultAuthorTransform(data) {
2422
+ const obj = data;
2423
+ const authorObj = tryFields(obj, "user", "author", "creator", "owner") ?? obj;
2424
+ const author = authorObj;
2425
+ return {
2426
+ id: toSafeString(tryFields(author, "id", "user_id", "author_id"), ""),
2427
+ name: toSafeString(
2428
+ tryFields(author, "display_name", "name", "username", "nickname"),
2429
+ "Unknown"
2430
+ ),
2431
+ avatar: toSafeString(
2432
+ tryFields(author, "avatar", "avatar_url", "profile_picture", "photo"),
2433
+ void 0
2434
+ ),
2435
+ isVerified: toBoolean(tryFields(author, "is_verified", "verified"), false)
2436
+ };
2437
+ }
2438
+ function defaultStatsTransform(data) {
2439
+ const obj = data;
2440
+ const statsObj = tryFields(obj, "stats", "statistics", "metrics") ?? obj;
2441
+ const stats = statsObj;
2442
+ return {
2443
+ views: toNumber(tryFields(stats, "view_count", "views", "play_count"), 0),
2444
+ likes: toNumber(tryFields(stats, "like_count", "likes", "digg_count"), 0),
2445
+ comments: toNumber(tryFields(stats, "comment_count", "comments"), 0),
2446
+ shares: toNumber(tryFields(stats, "share_count", "shares"), 0)
2447
+ };
2448
+ }
2449
+ function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
2450
+ const obj = apiResponse;
2451
+ const getMapped = (sdkField, ...fallbacks) => {
2452
+ if (fieldMap?.[sdkField]) {
2453
+ const mapped = getNestedValue(obj, fieldMap[sdkField]);
2454
+ if (mapped !== void 0) return mapped;
2455
+ }
2456
+ return tryFields(obj, ...fallbacks);
2457
+ };
2458
+ const id = toSafeString(getMapped("id", "id", "video_id", "_id"), "");
2459
+ const sourceUrl = toSafeString(
2460
+ getMapped("source.url", "video_url", "url", "source_url", "playback_url"),
2461
+ ""
2462
+ );
2463
+ if (!id) {
2464
+ logger?.warn("[Transform] Missing required field: id", { data: obj });
2465
+ }
2466
+ if (!sourceUrl) {
2467
+ logger?.warn("[Transform] Missing required field: source.url", { data: obj });
2468
+ }
2469
+ const source = defaultSourceTransform(obj);
2470
+ const author = defaultAuthorTransform(obj);
2471
+ const stats = defaultStatsTransform(obj);
2472
+ const videoItem = {
2473
+ id,
2474
+ source,
2475
+ poster: toSafeString(
2476
+ getMapped("poster", "thumbnail_url", "poster", "thumbnail", "cover", "cover_url"),
2477
+ void 0
2478
+ ),
2479
+ duration: toNumber(
2480
+ getMapped("duration", "duration", "duration_seconds", "length", "video_duration"),
2481
+ 0
2482
+ ),
2483
+ title: toSafeString(getMapped("title", "title", "description", "caption", "text"), void 0),
2484
+ author,
2485
+ stats,
2486
+ isLiked: toBoolean(getMapped("isLiked", "is_liked", "liked", "user_liked", "has_liked"), false),
2487
+ isFollowing: toBoolean(
2488
+ getMapped("isFollowing", "is_following", "following", "user_following"),
2489
+ false
2490
+ ),
2491
+ createdAt: toSafeString(
2492
+ getMapped("createdAt", "created_at", "createdAt", "create_time", "timestamp"),
2493
+ (/* @__PURE__ */ new Date()).toISOString()
2494
+ ),
2495
+ hashtags: getMapped("hashtags", "hashtags", "tags", "hash_tags") ?? []
2496
+ };
2497
+ return videoItem;
2498
+ }
2499
+ function defaultFeedResponseTransform(apiResponse, fieldMap, logger) {
2500
+ const obj = apiResponse;
2501
+ const itemsPath = fieldMap?.items;
2502
+ let items;
2503
+ if (itemsPath) {
2504
+ items = getNestedValue(obj, itemsPath) ?? [];
2505
+ } else {
2506
+ items = tryFields(obj, "data", "items", "videos", "results", "list") ?? [];
2507
+ }
2508
+ if (!Array.isArray(items)) {
2509
+ logger?.warn("[Transform] Feed items is not an array", { data: obj });
2510
+ items = [];
2511
+ }
2512
+ const cursorPath = fieldMap?.nextCursor;
2513
+ let nextCursor;
2514
+ if (cursorPath) {
2515
+ nextCursor = toStringOrNull(getNestedValue(obj, cursorPath));
2516
+ } else {
2517
+ nextCursor = toStringOrNull(
2518
+ tryFields(
2519
+ obj,
2520
+ "next_cursor",
2521
+ "nextCursor",
2522
+ "cursor",
2523
+ "next_page",
2524
+ "pagination.next",
2525
+ "meta.next_cursor"
2526
+ )
2527
+ );
2528
+ }
2529
+ const hasMorePath = fieldMap?.hasMore;
2530
+ let hasMore;
2531
+ if (hasMorePath) {
2532
+ hasMore = toBoolean(getNestedValue(obj, hasMorePath), false);
2533
+ } else {
2534
+ const hasMoreValue = tryFields(
2535
+ obj,
2536
+ "has_more",
2537
+ "hasMore",
2538
+ "has_next",
2539
+ "hasNext",
2540
+ "pagination.has_more",
2541
+ "meta.has_more"
2542
+ );
2543
+ if (hasMoreValue !== void 0) {
2544
+ hasMore = toBoolean(hasMoreValue, false);
2545
+ } else {
2546
+ hasMore = nextCursor !== null;
2547
+ }
2548
+ }
2549
+ return { items, nextCursor, hasMore };
2550
+ }
2551
+ function createTransforms(config, logger) {
2552
+ return {
2553
+ videoItem: config?.videoItem ? config.videoItem : (data) => defaultVideoItemTransform(data, config?.fieldMap?.video, logger),
2554
+ feedResponse: config?.feedResponse ? config.feedResponse : (data) => defaultFeedResponseTransform(data, config?.fieldMap?.feed, logger)
2555
+ };
2556
+ }
2557
+
2558
+ // src/preset/createRESTAdapters.ts
2559
+ function validateConfig(config) {
2560
+ if (!config.baseUrl) {
2561
+ throw new Error("[createRESTAdapters] baseUrl is required");
2562
+ }
2563
+ if (!config.auth?.getAccessToken) {
2564
+ throw new Error("[createRESTAdapters] auth.getAccessToken is required");
2565
+ }
2566
+ if (!config.endpoints?.feed?.list) {
2567
+ throw new Error("[createRESTAdapters] endpoints.feed.list is required");
2568
+ }
2569
+ if (!config.endpoints?.feed?.detail) {
2570
+ throw new Error("[createRESTAdapters] endpoints.feed.detail is required");
2571
+ }
2572
+ const requiredInteractionEndpoints = [
2573
+ "like",
2574
+ "unlike",
2575
+ "follow",
2576
+ "unfollow",
2577
+ "comment",
2578
+ "deleteComment"
2579
+ ];
2580
+ for (const endpoint of requiredInteractionEndpoints) {
2581
+ if (!config.endpoints.interaction?.[endpoint]) {
2582
+ throw new Error(`[createRESTAdapters] endpoints.interaction.${endpoint} is required`);
2583
+ }
2584
+ }
2585
+ }
2586
+ function createRESTAdapters(config) {
2587
+ validateConfig(config);
2588
+ const { baseUrl, auth, endpoints, transforms, request, logger } = config;
2589
+ const httpClient = new HttpClient({
2590
+ baseUrl,
2591
+ auth,
2592
+ request,
2593
+ logger
2594
+ });
2595
+ const resolvedTransforms = createTransforms(transforms, logger);
2596
+ const dataSource = new RESTDataAdapter({
2597
+ httpClient,
2598
+ endpoints: endpoints.feed,
2599
+ transforms: resolvedTransforms,
2600
+ pagination: request?.pagination,
2601
+ logger
2602
+ });
2603
+ const interaction = new RESTInteractionAdapter({
2604
+ httpClient,
2605
+ endpoints: endpoints.interaction,
2606
+ logger
2607
+ });
2608
+ const analytics = endpoints.analytics ? new RESTAnalyticsAdapter({
2609
+ httpClient,
2610
+ endpoints: endpoints.analytics,
2611
+ logger
2612
+ }) : createNoOpAnalyticsAdapter();
2613
+ return {
2614
+ dataSource,
2615
+ interaction,
2616
+ analytics
2617
+ };
2618
+ }
2619
+
2620
+ // src/preset/adapters/BrowserVideoLoader.ts
2621
+ var DEFAULT_CONFIG = {
2622
+ defaultMaxBytes: 512 * 1024,
2623
+ // 512KB
2624
+ defaultTimeout: 1e4,
2625
+ useCache: true,
2626
+ cacheName: "sv-video-cache"
2627
+ };
2628
+ var BrowserVideoLoader = class {
2629
+ constructor(config = {}) {
2630
+ this.preloads = /* @__PURE__ */ new Map();
2631
+ this.cache = null;
2632
+ this.logger = config.logger;
2633
+ this.config = {
2634
+ defaultMaxBytes: config.defaultMaxBytes ?? DEFAULT_CONFIG.defaultMaxBytes,
2635
+ defaultTimeout: config.defaultTimeout ?? DEFAULT_CONFIG.defaultTimeout,
2636
+ useCache: config.useCache ?? DEFAULT_CONFIG.useCache,
2637
+ cacheName: config.cacheName ?? DEFAULT_CONFIG.cacheName
2638
+ };
2639
+ this.initCache();
2640
+ }
2641
+ /**
2642
+ * Initialize Cache API
2643
+ */
2644
+ async initCache() {
2645
+ if (!this.config.useCache) return;
2646
+ try {
2647
+ if ("caches" in window) {
2648
+ this.cache = await caches.open(this.config.cacheName);
2649
+ this.logger?.debug("[BrowserVideoLoader] Cache initialized");
2650
+ }
2651
+ } catch (error) {
2652
+ this.logger?.warn("[BrowserVideoLoader] Cache API not available", { error });
2653
+ }
2654
+ }
2655
+ /**
2656
+ * Preload video source
2657
+ */
2658
+ async preload(videoId, source, config) {
2659
+ const existing = this.preloads.get(videoId);
2660
+ if (existing?.status === "ready") {
2661
+ return {
2662
+ videoId,
2663
+ status: "ready",
2664
+ loadedBytes: existing.loadedBytes
2665
+ };
2666
+ }
2667
+ if (existing?.status === "loading") {
2668
+ this.logger?.debug("[BrowserVideoLoader] Already loading", { videoId });
2669
+ return { videoId, status: "loading" };
2670
+ }
2671
+ const abortController = new AbortController();
2672
+ const timeout = config?.timeout ?? this.config.defaultTimeout;
2673
+ const maxBytes = config?.maxBytes ?? this.config.defaultMaxBytes;
2674
+ this.preloads.set(videoId, {
2675
+ status: "loading",
2676
+ loadedBytes: 0,
2677
+ abortController
2678
+ });
2679
+ const timeoutId = setTimeout(() => {
2680
+ abortController.abort();
2681
+ }, timeout);
2682
+ try {
2683
+ let loadedBytes;
2684
+ if (source.type === "hls") {
2685
+ loadedBytes = await this.preloadHLS(source.url, abortController.signal);
2686
+ } else {
2687
+ loadedBytes = await this.preloadMP4(source.url, maxBytes, abortController.signal);
2688
+ }
2689
+ clearTimeout(timeoutId);
2690
+ this.preloads.set(videoId, {
2691
+ status: "ready",
2692
+ loadedBytes
2693
+ });
2694
+ this.logger?.debug("[BrowserVideoLoader] Preload complete", {
2695
+ videoId,
2696
+ loadedBytes,
2697
+ type: source.type
2698
+ });
2699
+ return {
2700
+ videoId,
2701
+ status: "ready",
2702
+ loadedBytes
2703
+ };
2704
+ } catch (error) {
2705
+ clearTimeout(timeoutId);
2706
+ const err = error instanceof Error ? error : new Error(String(error));
2707
+ if (err.name !== "AbortError") {
2708
+ this.logger?.warn("[BrowserVideoLoader] Preload failed", { videoId, error: err.message });
2709
+ }
2710
+ this.preloads.set(videoId, {
2711
+ status: err.name === "AbortError" ? "idle" : "error",
2712
+ loadedBytes: 0,
2713
+ error: err
2714
+ });
2715
+ return {
2716
+ videoId,
2717
+ status: err.name === "AbortError" ? "idle" : "error",
2718
+ error: err
2719
+ };
2720
+ }
2721
+ }
2722
+ /**
2723
+ * Preload MP4 video using Range request
2724
+ */
2725
+ async preloadMP4(url, maxBytes, signal) {
2726
+ const response = await fetch(url, {
2727
+ method: "GET",
2728
+ headers: {
2729
+ Range: `bytes=0-${maxBytes - 1}`
2730
+ },
2731
+ signal
2732
+ });
2733
+ if (!response.ok && response.status !== 206) {
2734
+ throw new Error(`HTTP error: ${response.status}`);
2735
+ }
2736
+ const buffer = await response.arrayBuffer();
2737
+ if (this.cache && buffer.byteLength > 0) {
2738
+ try {
2739
+ const cacheResponse = new Response(buffer, {
2740
+ headers: {
2741
+ "Content-Type": "video/mp4",
2742
+ "Content-Length": String(buffer.byteLength)
2743
+ }
2744
+ });
2745
+ await this.cache.put(url, cacheResponse);
2746
+ } catch {
2747
+ }
2748
+ }
2749
+ return buffer.byteLength;
2750
+ }
2751
+ /**
2752
+ * Preload HLS - fetch manifest only
2753
+ * Full segment preload requires hls.js which is optional
2754
+ */
2755
+ async preloadHLS(url, signal) {
2756
+ const response = await fetch(url, { signal });
2757
+ if (!response.ok) {
2758
+ throw new Error(`HTTP error: ${response.status}`);
2759
+ }
2760
+ const text = await response.text();
2761
+ if (this.cache) {
2762
+ try {
2763
+ const cacheResponse = new Response(text, {
2764
+ headers: {
2765
+ "Content-Type": "application/vnd.apple.mpegurl"
2766
+ }
2767
+ });
2768
+ await this.cache.put(url, cacheResponse);
2769
+ } catch {
2770
+ }
2771
+ }
2772
+ return text.length;
2773
+ }
2774
+ /**
2775
+ * Cancel preload
2776
+ */
2777
+ cancelPreload(videoId) {
2778
+ const state = this.preloads.get(videoId);
2779
+ if (state?.abortController) {
2780
+ state.abortController.abort();
2781
+ }
2782
+ this.preloads.set(videoId, {
2783
+ status: "idle",
2784
+ loadedBytes: 0
2785
+ });
2786
+ this.logger?.debug("[BrowserVideoLoader] Cancelled preload", { videoId });
2787
+ }
2788
+ /**
2789
+ * Check if video is preloaded
2790
+ */
2791
+ isPreloaded(videoId) {
2792
+ return this.preloads.get(videoId)?.status === "ready";
2793
+ }
2794
+ /**
2795
+ * Get preload status
2796
+ */
2797
+ getPreloadStatus(videoId) {
2798
+ return this.preloads.get(videoId)?.status ?? "idle";
2799
+ }
2800
+ /**
2801
+ * Clear preloaded data for a video
2802
+ */
2803
+ clearPreload(videoId) {
2804
+ this.preloads.delete(videoId);
2805
+ }
2806
+ /**
2807
+ * Clear all preloaded data
2808
+ */
2809
+ clearAll() {
2810
+ for (const [, state] of this.preloads) {
2811
+ if (state.abortController) {
2812
+ state.abortController.abort();
2813
+ }
2814
+ }
2815
+ this.preloads.clear();
2816
+ if (this.cache) {
2817
+ caches.delete(this.config.cacheName).catch(() => {
2818
+ });
2819
+ }
2820
+ this.logger?.debug("[BrowserVideoLoader] Cleared all preloads");
2821
+ }
2822
+ /**
2823
+ * Get total preloaded bytes
2824
+ */
2825
+ getTotalPreloadedBytes() {
2826
+ let total = 0;
2827
+ for (const state of this.preloads.values()) {
2828
+ if (state.status === "ready") {
2829
+ total += state.loadedBytes;
2830
+ }
2831
+ }
2832
+ return total;
2833
+ }
2834
+ };
2835
+ var BrowserPosterLoader = class {
2836
+ constructor(config) {
2837
+ this.cache = /* @__PURE__ */ new Set();
2838
+ this.pending = /* @__PURE__ */ new Map();
2839
+ this.logger = config?.logger;
2840
+ this.timeout = config?.timeout ?? 1e4;
2841
+ }
2842
+ /**
2843
+ * Preload a poster image
2844
+ */
2845
+ async preload(url) {
2846
+ if (this.cache.has(url)) {
2847
+ return;
2848
+ }
2849
+ const pending = this.pending.get(url);
2850
+ if (pending) {
2851
+ return pending;
2852
+ }
2853
+ const promise = new Promise((resolve, reject) => {
2854
+ const img = new Image();
2855
+ const timeoutId = setTimeout(() => {
2856
+ img.src = "";
2857
+ reject(new Error("Poster preload timeout"));
2858
+ }, this.timeout);
2859
+ img.onload = () => {
2860
+ clearTimeout(timeoutId);
2861
+ this.cache.add(url);
2862
+ this.pending.delete(url);
2863
+ this.logger?.debug("[BrowserPosterLoader] Preloaded", { url });
2864
+ resolve();
2865
+ };
2866
+ img.onerror = () => {
2867
+ clearTimeout(timeoutId);
2868
+ this.pending.delete(url);
2869
+ this.logger?.warn("[BrowserPosterLoader] Failed to preload", { url });
2870
+ reject(new Error(`Failed to load image: ${url}`));
2871
+ };
2872
+ img.src = url;
2873
+ });
2874
+ this.pending.set(url, promise);
2875
+ try {
2876
+ await promise;
2877
+ } catch {
2878
+ }
2879
+ }
2880
+ /**
2881
+ * Check if poster is cached
2882
+ */
2883
+ isCached(url) {
2884
+ return this.cache.has(url);
2885
+ }
2886
+ /**
2887
+ * Clear poster cache
2888
+ */
2889
+ clearCache() {
2890
+ this.cache.clear();
2891
+ this.pending.clear();
2892
+ this.logger?.debug("[BrowserPosterLoader] Cache cleared");
2893
+ }
2894
+ };
2895
+ function createBrowserVideoLoader(config) {
2896
+ return new BrowserVideoLoader(config);
2897
+ }
2898
+ function createBrowserPosterLoader(config) {
2899
+ return new BrowserPosterLoader(config);
2900
+ }
2901
+
2902
+ // src/preset/adapters/LocalStorageAdapter.ts
2903
+ var DEFAULT_CONFIG2 = {
2904
+ prefix: "sv-",
2905
+ maxSnapshotAge: 24 * 60 * 60 * 1e3
2906
+ // 24 hours
2907
+ };
2908
+ var SESSION_SNAPSHOT_KEY2 = "session-snapshot";
2909
+ var LocalStorageAdapter = class {
2910
+ constructor(config = {}) {
2911
+ this.prefix = config.prefix ?? DEFAULT_CONFIG2.prefix;
2912
+ this.logger = config.logger;
2913
+ }
2914
+ /**
2915
+ * Build namespaced key
2916
+ */
2917
+ buildKey(key) {
2918
+ return `${this.prefix}${key}`;
2919
+ }
2920
+ /**
2921
+ * Get a value from localStorage
2922
+ */
2923
+ async get(key) {
2924
+ try {
2925
+ const fullKey = this.buildKey(key);
2926
+ const value = localStorage.getItem(fullKey);
2927
+ if (value === null) {
2928
+ return null;
2929
+ }
2930
+ return JSON.parse(value);
2931
+ } catch (error) {
2932
+ this.logger?.warn("[LocalStorageAdapter] Failed to get/parse value", { key, error });
2933
+ return null;
2934
+ }
2935
+ }
2936
+ /**
2937
+ * Set a value in localStorage
2938
+ */
2939
+ async set(key, value) {
2940
+ try {
2941
+ const fullKey = this.buildKey(key);
2942
+ localStorage.setItem(fullKey, JSON.stringify(value));
2943
+ } catch (error) {
2944
+ if (error instanceof DOMException && error.name === "QuotaExceededError") {
2945
+ this.logger?.warn("[LocalStorageAdapter] Storage quota exceeded, clearing old data");
2946
+ await this.clearOldEntries();
2947
+ try {
2948
+ const fullKey = this.buildKey(key);
2949
+ localStorage.setItem(fullKey, JSON.stringify(value));
2950
+ } catch {
2951
+ this.logger?.error(
2952
+ "[LocalStorageAdapter] Still over quota after cleanup",
2953
+ error
2954
+ );
2955
+ throw error;
2956
+ }
2957
+ } else {
2958
+ throw error;
2959
+ }
2960
+ }
2961
+ }
2962
+ /**
2963
+ * Remove a value from localStorage
2964
+ */
2965
+ async remove(key) {
2966
+ const fullKey = this.buildKey(key);
2967
+ localStorage.removeItem(fullKey);
2968
+ }
2969
+ /**
2970
+ * Clear all SDK-namespaced keys
2971
+ */
2972
+ async clear() {
2973
+ const keysToRemove = [];
2974
+ for (let i = 0; i < localStorage.length; i++) {
2975
+ const key = localStorage.key(i);
2976
+ if (key?.startsWith(this.prefix)) {
2977
+ keysToRemove.push(key);
2978
+ }
2979
+ }
2980
+ for (const key of keysToRemove) {
2981
+ localStorage.removeItem(key);
2982
+ }
2983
+ this.logger?.debug("[LocalStorageAdapter] Cleared all SDK keys", {
2984
+ count: keysToRemove.length
2985
+ });
2986
+ }
2987
+ /**
2988
+ * Get all SDK-namespaced keys
2989
+ */
2990
+ async keys() {
2991
+ const sdkKeys = [];
2992
+ for (let i = 0; i < localStorage.length; i++) {
2993
+ const key = localStorage.key(i);
2994
+ if (key?.startsWith(this.prefix)) {
2995
+ sdkKeys.push(key.slice(this.prefix.length));
2996
+ }
2997
+ }
2998
+ return sdkKeys;
2999
+ }
3000
+ /**
3001
+ * Clear old entries when quota exceeded (simple LRU-like behavior)
3002
+ */
3003
+ async clearOldEntries() {
3004
+ const allKeys = await this.keys();
3005
+ const keysToRemove = allKeys.slice(0, Math.ceil(allKeys.length / 2));
3006
+ for (const key of keysToRemove) {
3007
+ await this.remove(key);
3008
+ }
3009
+ this.logger?.debug("[LocalStorageAdapter] Cleared old entries", { count: keysToRemove.length });
3010
+ }
3011
+ };
3012
+ var LocalSessionStorageAdapter = class extends LocalStorageAdapter {
3013
+ constructor(config = {}) {
3014
+ super(config);
3015
+ this.maxSnapshotAge = config.maxSnapshotAge ?? DEFAULT_CONFIG2.maxSnapshotAge;
3016
+ }
3017
+ /**
3018
+ * Save session snapshot
3019
+ */
3020
+ async saveSnapshot(snapshot) {
3021
+ await this.set(SESSION_SNAPSHOT_KEY2, snapshot);
3022
+ this.logger?.debug("[LocalSessionStorageAdapter] Saved snapshot", {
3023
+ index: snapshot.focusedIndex,
3024
+ cursor: snapshot.cursor
3025
+ });
3026
+ }
3027
+ /**
3028
+ * Load session snapshot (returns null if stale)
3029
+ */
3030
+ async loadSnapshot() {
3031
+ const snapshot = await this.get(SESSION_SNAPSHOT_KEY2);
3032
+ if (!snapshot) {
3033
+ return null;
3034
+ }
3035
+ const age = Date.now() - snapshot.savedAt;
3036
+ if (age > this.maxSnapshotAge) {
3037
+ this.logger?.debug("[LocalSessionStorageAdapter] Snapshot is stale, clearing", {
3038
+ age,
3039
+ maxAge: this.maxSnapshotAge
3040
+ });
3041
+ await this.clearSnapshot();
3042
+ return null;
3043
+ }
3044
+ this.logger?.debug("[LocalSessionStorageAdapter] Loaded snapshot", {
3045
+ index: snapshot.focusedIndex,
3046
+ age
3047
+ });
3048
+ return snapshot;
3049
+ }
3050
+ /**
3051
+ * Clear session snapshot
3052
+ */
3053
+ async clearSnapshot() {
3054
+ await this.remove(SESSION_SNAPSHOT_KEY2);
3055
+ this.logger?.debug("[LocalSessionStorageAdapter] Cleared snapshot");
3056
+ }
3057
+ };
3058
+ function createLocalStorageAdapter(config) {
3059
+ return new LocalStorageAdapter(config);
3060
+ }
3061
+ function createSessionStorageAdapter(config) {
3062
+ return new LocalSessionStorageAdapter(config);
3063
+ }
3064
+
3065
+ // src/preset/adapters/WebNetworkAdapter.ts
3066
+ function mapEffectiveType(effectiveType) {
3067
+ switch (effectiveType) {
3068
+ case "4g":
3069
+ return "4g";
3070
+ case "3g":
3071
+ return "3g";
3072
+ case "2g":
3073
+ case "slow-2g":
3074
+ return "slow";
3075
+ default:
3076
+ return "wifi";
3077
+ }
3078
+ }
3079
+ function mapConnectionType(type, effectiveType) {
3080
+ if (type === "none") return "offline";
3081
+ if (type === "wifi" || type === "ethernet") return "wifi";
3082
+ if (type === "cellular") {
3083
+ return mapEffectiveType(effectiveType);
3084
+ }
3085
+ return mapEffectiveType(effectiveType);
3086
+ }
3087
+ var WebNetworkAdapter = class {
3088
+ constructor(config = {}) {
3089
+ this.listeners = /* @__PURE__ */ new Set();
3090
+ this.connection = null;
3091
+ this.logger = config.logger;
3092
+ this.fallbackType = config.fallbackType ?? "wifi";
3093
+ this.fallbackDownlink = config.fallbackDownlink ?? 10;
3094
+ const nav = navigator;
3095
+ this.connection = nav.connection ?? nav.mozConnection ?? nav.webkitConnection ?? null;
3096
+ this.boundOnlineHandler = this.handleOnline.bind(this);
3097
+ this.boundOfflineHandler = this.handleOffline.bind(this);
3098
+ this.boundChangeHandler = this.handleConnectionChange.bind(this);
3099
+ this.setupListeners();
3100
+ this.logger?.debug("[WebNetworkAdapter] Initialized", {
3101
+ hasConnectionAPI: !!this.connection
3102
+ });
3103
+ }
3104
+ /**
3105
+ * Get current network type
3106
+ */
3107
+ async getNetworkType() {
3108
+ if (!navigator.onLine) {
3109
+ return "offline";
3110
+ }
3111
+ if (!this.connection) {
3112
+ return this.fallbackType;
3113
+ }
3114
+ return mapConnectionType(this.connection.type, this.connection.effectiveType);
3115
+ }
3116
+ /**
3117
+ * Get detailed network quality
3118
+ */
3119
+ async getNetworkQuality() {
3120
+ const type = await this.getNetworkType();
3121
+ if (!this.connection) {
3122
+ return {
3123
+ type,
3124
+ downlink: this.fallbackDownlink,
3125
+ rtt: void 0,
3126
+ isMetered: false
3127
+ };
3128
+ }
3129
+ return {
3130
+ type,
3131
+ downlink: this.connection.downlink,
3132
+ rtt: this.connection.rtt,
3133
+ isMetered: this.connection.saveData ?? this.connection.type === "cellular"
3134
+ };
3135
+ }
3136
+ /**
3137
+ * Subscribe to network changes
3138
+ */
3139
+ onNetworkChange(callback) {
3140
+ this.listeners.add(callback);
3141
+ return () => {
3142
+ this.listeners.delete(callback);
3143
+ };
3144
+ }
3145
+ /**
3146
+ * Check if currently online
3147
+ */
3148
+ async isOnline() {
3149
+ return navigator.onLine;
3150
+ }
3151
+ /**
3152
+ * Cleanup listeners
3153
+ */
3154
+ destroy() {
3155
+ window.removeEventListener("online", this.boundOnlineHandler);
3156
+ window.removeEventListener("offline", this.boundOfflineHandler);
3157
+ if (this.connection) {
3158
+ this.connection.removeEventListener("change", this.boundChangeHandler);
3159
+ }
3160
+ this.listeners.clear();
3161
+ this.logger?.debug("[WebNetworkAdapter] Destroyed");
3162
+ }
3163
+ // ═══════════════════════════════════════════════════════════════
3164
+ // PRIVATE METHODS
3165
+ // ═══════════════════════════════════════════════════════════════
3166
+ setupListeners() {
3167
+ window.addEventListener("online", this.boundOnlineHandler);
3168
+ window.addEventListener("offline", this.boundOfflineHandler);
3169
+ if (this.connection) {
3170
+ this.connection.addEventListener("change", this.boundChangeHandler);
3171
+ }
3172
+ }
3173
+ handleOnline() {
3174
+ this.logger?.debug("[WebNetworkAdapter] Online");
3175
+ this.notifyListeners();
3176
+ }
3177
+ handleOffline() {
3178
+ this.logger?.debug("[WebNetworkAdapter] Offline");
3179
+ this.notifyListeners();
3180
+ }
3181
+ handleConnectionChange() {
3182
+ this.logger?.debug("[WebNetworkAdapter] Connection changed", {
3183
+ type: this.connection?.type,
3184
+ effectiveType: this.connection?.effectiveType
3185
+ });
3186
+ this.notifyListeners();
3187
+ }
3188
+ async notifyListeners() {
3189
+ const type = await this.getNetworkType();
3190
+ for (const listener of this.listeners) {
3191
+ try {
3192
+ listener(type);
3193
+ } catch (error) {
3194
+ this.logger?.error("[WebNetworkAdapter] Listener error", error);
3195
+ }
3196
+ }
3197
+ }
3198
+ };
3199
+ function createWebNetworkAdapter(config) {
3200
+ return new WebNetworkAdapter(config);
3201
+ }
3202
+
3203
+ // src/preset/createBrowserAdapters.ts
3204
+ function createBrowserAdapters(config) {
3205
+ const restAdapters = createRESTAdapters(config);
3206
+ const storage = new LocalSessionStorageAdapter({
3207
+ ...config.storage,
3208
+ logger: config.logger
3209
+ });
3210
+ const network = new WebNetworkAdapter({
3211
+ ...config.network,
3212
+ logger: config.logger
3213
+ });
3214
+ const videoLoader = new BrowserVideoLoader({
3215
+ ...config.videoLoader,
3216
+ logger: config.logger
3217
+ });
3218
+ const posterLoader = new BrowserPosterLoader({
3219
+ logger: config.logger,
3220
+ timeout: config.posterTimeout
3221
+ });
3222
+ config.logger?.info("[createBrowserAdapters] Created all adapters", {
3223
+ baseUrl: config.baseUrl,
3224
+ hasAnalytics: !!config.endpoints.analytics
3225
+ });
3226
+ return {
3227
+ dataSource: restAdapters.dataSource,
3228
+ interaction: restAdapters.interaction,
3229
+ analytics: restAdapters.analytics,
3230
+ storage,
3231
+ network,
3232
+ videoLoader,
3233
+ posterLoader,
3234
+ logger: config.logger
3235
+ };
3236
+ }
3237
+
3238
+ export { BrowserPosterLoader, BrowserVideoLoader, DEFAULT_REQUEST_CONFIG, DEFAULT_RETRY_CONFIG, HttpClient, HttpError, LocalSessionStorageAdapter, LocalStorageAdapter, MockAnalyticsAdapter, MockDataAdapter, MockInteractionAdapter, MockLoggerAdapter, MockNetworkAdapter, MockPosterLoader, MockSessionStorageAdapter, MockStorageAdapter, MockVideoLoader, RESTAnalyticsAdapter, RESTDataAdapter, RESTInteractionAdapter, WebNetworkAdapter, createBrowserAdapters, createBrowserPosterLoader, createBrowserVideoLoader, createLocalStorageAdapter, createNoOpAnalyticsAdapter, createRESTAdapters, createSessionStorageAdapter, createTransforms, createWebNetworkAdapter, defaultFeedResponseTransform, defaultVideoItemTransform };