@xhub-short/adapters 0.1.0-beta.8 → 1.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +541 -19
  2. package/dist/index.js +692 -67
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ var MOCK_VIDEOS = [
4
4
  // HLS Videos (for testing hls.js integration)
5
5
  // ═══════════════════════════════════════════════════════════════════════════
6
6
  {
7
+ type: "video",
7
8
  id: "video-1",
8
9
  source: {
9
10
  url: "https://peertube.teknix.services/static/streaming-playlists/hls/dd8de71d-0b75-4677-a1a2-6f60e673bee4/465faffa-6d08-4f34-ae40-691cc904ce7b-master.m3u8",
@@ -30,6 +31,7 @@ var MOCK_VIDEOS = [
30
31
  hashtags: ["hls", "streaming", "test"]
31
32
  },
32
33
  {
34
+ type: "video",
33
35
  id: "video-2",
34
36
  source: {
35
37
  url: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8",
@@ -59,6 +61,7 @@ var MOCK_VIDEOS = [
59
61
  // MP4 Videos
60
62
  // ═══════════════════════════════════════════════════════════════════════════
61
63
  {
64
+ type: "video",
62
65
  id: "video-3",
63
66
  source: {
64
67
  url: "https://peertube.teknix.services/static/streaming-playlists/hls/ea58b245-b3bf-4958-b2f0-b31f8113d142/83f1bb91-e76a-4dcd-8034-2ec37cb70ead-master.m3u8",
@@ -85,6 +88,7 @@ var MOCK_VIDEOS = [
85
88
  hashtags: ["chrome", "blazes"]
86
89
  },
87
90
  {
91
+ type: "video",
88
92
  id: "video-4",
89
93
  source: {
90
94
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
@@ -111,6 +115,7 @@ var MOCK_VIDEOS = [
111
115
  hashtags: ["chrome", "escapes"]
112
116
  },
113
117
  {
118
+ type: "video",
114
119
  id: "video-5",
115
120
  source: {
116
121
  url: "https://peertube.teknix.services/static/streaming-playlists/hls/003a41a3-25c1-419b-9548-7a1597adc85f/9cc93564-c9c9-4107-ae12-cd22d0db8046-master.m3u8",
@@ -137,6 +142,7 @@ var MOCK_VIDEOS = [
137
142
  hashtags: ["chrome", "fun"]
138
143
  },
139
144
  {
145
+ type: "video",
140
146
  id: "video-6",
141
147
  source: {
142
148
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
@@ -163,6 +169,7 @@ var MOCK_VIDEOS = [
163
169
  hashtags: ["adventure", "joyride", "travel"]
164
170
  },
165
171
  {
172
+ type: "video",
166
173
  id: "video-7",
167
174
  source: {
168
175
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
@@ -189,6 +196,7 @@ var MOCK_VIDEOS = [
189
196
  hashtags: ["satisfying", "icecream", "asmr"]
190
197
  },
191
198
  {
199
+ type: "video",
192
200
  id: "video-8",
193
201
  source: {
194
202
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
@@ -215,6 +223,7 @@ var MOCK_VIDEOS = [
215
223
  hashtags: ["fantasy", "animation", "sintel", "blender"]
216
224
  },
217
225
  {
226
+ type: "video",
218
227
  id: "video-9",
219
228
  source: {
220
229
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4",
@@ -241,6 +250,7 @@ var MOCK_VIDEOS = [
241
250
  hashtags: ["cars", "subaru", "offroad", "review"]
242
251
  },
243
252
  {
253
+ type: "video",
244
254
  id: "video-10",
245
255
  source: {
246
256
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
@@ -267,6 +277,7 @@ var MOCK_VIDEOS = [
267
277
  hashtags: ["scifi", "drama", "blender", "vfx"]
268
278
  },
269
279
  {
280
+ type: "video",
270
281
  id: "video-11",
271
282
  source: {
272
283
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4",
@@ -293,6 +304,7 @@ var MOCK_VIDEOS = [
293
304
  hashtags: ["cars", "vw", "gti", "hothatch"]
294
305
  },
295
306
  {
307
+ type: "video",
296
308
  id: "video-12",
297
309
  source: {
298
310
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4",
@@ -319,6 +331,7 @@ var MOCK_VIDEOS = [
319
331
  hashtags: ["rally", "racing", "bullrun", "supercars"]
320
332
  },
321
333
  {
334
+ type: "video",
322
335
  id: "video-13",
323
336
  source: {
324
337
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4",
@@ -345,6 +358,7 @@ var MOCK_VIDEOS = [
345
358
  hashtags: ["budget", "usedcars", "tips", "bargain"]
346
359
  },
347
360
  {
361
+ type: "video",
348
362
  id: "video-14",
349
363
  source: {
350
364
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
@@ -371,6 +385,7 @@ var MOCK_VIDEOS = [
371
385
  hashtags: ["bts", "animation", "3d", "making"]
372
386
  },
373
387
  {
388
+ type: "video",
374
389
  id: "video-15",
375
390
  source: {
376
391
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
@@ -397,6 +412,7 @@ var MOCK_VIDEOS = [
397
412
  hashtags: ["tutorial", "animation", "blender", "makingof"]
398
413
  },
399
414
  {
415
+ type: "video",
400
416
  id: "video-16",
401
417
  source: {
402
418
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
@@ -423,6 +439,7 @@ var MOCK_VIDEOS = [
423
439
  hashtags: ["characterdesign", "sintel", "tutorial", "art"]
424
440
  },
425
441
  {
442
+ type: "video",
426
443
  id: "video-17",
427
444
  source: {
428
445
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
@@ -449,6 +466,7 @@ var MOCK_VIDEOS = [
449
466
  hashtags: ["vfx", "breakdown", "compositing", "cgi"]
450
467
  },
451
468
  {
469
+ type: "video",
452
470
  id: "video-18",
453
471
  source: {
454
472
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
@@ -475,6 +493,7 @@ var MOCK_VIDEOS = [
475
493
  hashtags: ["pov", "roadtrip", "travel", "wanderlust"]
476
494
  },
477
495
  {
496
+ type: "video",
478
497
  id: "video-19",
479
498
  source: {
480
499
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
@@ -501,6 +520,7 @@ var MOCK_VIDEOS = [
501
520
  hashtags: ["satisfying", "fire", "asmr", "relaxing"]
502
521
  },
503
522
  {
523
+ type: "video",
504
524
  id: "video-20",
505
525
  source: {
506
526
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
@@ -529,20 +549,20 @@ var MOCK_VIDEOS = [
529
549
  ];
530
550
  var MockDataAdapter = class {
531
551
  constructor(options = {}) {
532
- this.videos = options.videos ?? MOCK_VIDEOS;
552
+ this.items = options.items ?? options.videos ?? MOCK_VIDEOS;
533
553
  this.pageSize = options.pageSize ?? 3;
534
554
  this.delay = options.delay ?? 300;
535
555
  }
536
556
  /**
537
- * Fetch a page of mock videos
557
+ * Fetch a page of mock items
538
558
  */
539
559
  async fetchFeed(cursor) {
540
560
  await this.simulateDelay();
541
561
  const offset = cursor ? Number.parseInt(cursor, 10) : 0;
542
562
  const start = offset;
543
563
  const end = start + this.pageSize;
544
- const items = this.videos.slice(start, end);
545
- const hasMore = end < this.videos.length;
564
+ const items = this.items.slice(start, end);
565
+ const hasMore = end < this.items.length;
546
566
  const nextCursor = hasMore ? String(end) : null;
547
567
  return {
548
568
  items,
@@ -551,15 +571,21 @@ var MockDataAdapter = class {
551
571
  };
552
572
  }
553
573
  /**
554
- * Get a single video by ID
574
+ * Get a single content item by ID
555
575
  */
556
- async getVideoDetail(id) {
576
+ async getContentDetail(id) {
557
577
  await this.simulateDelay();
558
- const video = this.videos.find((v) => v.id === id);
559
- if (!video) {
560
- throw new Error(`Video not found: ${id}`);
578
+ const item = this.items.find((v) => v.id === id);
579
+ if (!item) {
580
+ throw new Error(`Content not found: ${id}`);
561
581
  }
562
- return video;
582
+ return item;
583
+ }
584
+ /**
585
+ * @deprecated Use getContentDetail instead
586
+ */
587
+ async getVideoDetail(id) {
588
+ return this.getContentDetail(id);
563
589
  }
564
590
  /**
565
591
  * Prefetch videos (no-op for mock)
@@ -575,6 +601,87 @@ var MockDataAdapter = class {
575
601
  }
576
602
  }
577
603
  };
604
+ var MOCK_ITEMS = MOCK_VIDEOS;
605
+
606
+ // src/playlist/MockPlaylistAdapter.ts
607
+ var MOCK_PLAYLISTS = [
608
+ {
609
+ id: "p1",
610
+ title: "Workout Jams",
611
+ description: "High energy tracks for your workout",
612
+ cover: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&h=600&fit=crop",
613
+ totalItems: 4,
614
+ items: []
615
+ // Populated on fetchPlaylist
616
+ },
617
+ {
618
+ id: "p2",
619
+ title: "Chill Vibes",
620
+ description: "Relax and unwind with these lo-fi beats",
621
+ cover: "https://images.unsplash.com/photo-1516280440614-37939bbacd81?w=400&h=600&fit=crop",
622
+ totalItems: 4,
623
+ items: []
624
+ },
625
+ {
626
+ id: "p3",
627
+ title: "Travel Diaries",
628
+ description: "Explore the world through music and video",
629
+ cover: "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=400&h=600&fit=crop",
630
+ totalItems: 4,
631
+ items: []
632
+ },
633
+ {
634
+ id: "p4",
635
+ title: "Cooking with Chef loct",
636
+ description: "Delicious recipes and kitchen tips",
637
+ cover: "https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=400&h=600&fit=crop",
638
+ totalItems: 4,
639
+ items: []
640
+ }
641
+ ];
642
+ var MockPlaylistAdapter = class {
643
+ constructor(options = {}) {
644
+ this.delay = options.delay ?? 300;
645
+ }
646
+ async fetchPlaylist(id) {
647
+ await this.simulateDelay();
648
+ const playlist = MOCK_PLAYLISTS.find((p) => p.id === id);
649
+ if (!playlist) throw new Error(`Playlist ${id} not found`);
650
+ let items = [];
651
+ if (id === "p1") items = MOCK_ITEMS.slice(0, 4);
652
+ else if (id === "p2") items = MOCK_ITEMS.slice(4, 8);
653
+ else if (id === "p3") items = MOCK_ITEMS.slice(8, 12);
654
+ else if (id === "p4") items = MOCK_ITEMS.slice(12, 16);
655
+ return { ...playlist, items };
656
+ }
657
+ async fetchPlaylistCollection(cursor) {
658
+ await this.simulateDelay();
659
+ const offset = cursor ? Number.parseInt(cursor, 10) : 0;
660
+ const limit = 4;
661
+ const items = MOCK_PLAYLISTS.slice(offset, offset + limit);
662
+ const nextOffset = offset + limit;
663
+ const hasMore = nextOffset < MOCK_PLAYLISTS.length;
664
+ const playlists = items.map((p) => ({
665
+ id: p.id,
666
+ title: p.title,
667
+ description: p.description,
668
+ cover: p.cover,
669
+ totalItems: p.totalItems,
670
+ author: { id: "a1", name: "System" },
671
+ updatedAt: "2 days ago"
672
+ }));
673
+ return {
674
+ playlists,
675
+ nextCursor: hasMore ? String(nextOffset) : null,
676
+ hasMore
677
+ };
678
+ }
679
+ async simulateDelay() {
680
+ if (this.delay > 0) {
681
+ await new Promise((resolve) => setTimeout(resolve, this.delay));
682
+ }
683
+ }
684
+ };
578
685
 
579
686
  // src/logger/mock.ts
580
687
  var LOG_LEVEL_PRIORITY = {
@@ -1107,9 +1214,9 @@ var MockInteractionAdapter = class {
1107
1214
  /**
1108
1215
  * Report a video
1109
1216
  *
1110
- * @param _videoId - ID of the video to report
1111
- * @param _reason - Report reason code
1112
- * @param _description - Optional additional description
1217
+ * @param videoId - ID of the video to report
1218
+ * @param reason - Report reason code
1219
+ * @param description - Optional additional description
1113
1220
  */
1114
1221
  async report(_videoId, _reason, _description) {
1115
1222
  await this.simulateDelay();
@@ -1435,27 +1542,23 @@ var MockCommentAdapter = class {
1435
1542
  await this.simulateNetworkDelay();
1436
1543
  if (payload.isReply && payload.parentId) {
1437
1544
  const replies = this.replies.get(payload.parentId) ?? [];
1438
- const filtered = replies.filter((r) => r.id !== payload.id);
1545
+ const filtered = replies.filter((r) => r.id !== payload.commentId);
1439
1546
  this.replies.set(payload.parentId, filtered);
1440
- for (const [videoId, comments] of this.comments) {
1441
- const idx = comments.findIndex((c) => c.id === payload.parentId);
1442
- if (idx !== -1 && comments[idx]) {
1443
- comments[idx] = {
1444
- ...comments[idx],
1445
- replyCount: Math.max(0, comments[idx].replyCount - 1)
1446
- };
1447
- this.comments.set(videoId, comments);
1448
- break;
1449
- }
1547
+ const comments = this.comments.get(payload.videoId) ?? [];
1548
+ const idx = comments.findIndex((c) => c.id === payload.parentId);
1549
+ if (idx !== -1 && comments[idx]) {
1550
+ comments[idx] = {
1551
+ ...comments[idx],
1552
+ replyCount: Math.max(0, comments[idx].replyCount - 1)
1553
+ };
1554
+ this.comments.set(payload.videoId, comments);
1450
1555
  }
1451
1556
  } else {
1452
- for (const [videoId, comments] of this.comments) {
1453
- const filtered = comments.filter((c) => c.id !== payload.id);
1454
- if (filtered.length !== comments.length) {
1455
- this.comments.set(videoId, filtered);
1456
- this.replies.delete(payload.id);
1457
- break;
1458
- }
1557
+ const comments = this.comments.get(payload.videoId) ?? [];
1558
+ const filtered = comments.filter((c) => c.id !== payload.commentId);
1559
+ if (filtered.length !== comments.length) {
1560
+ this.comments.set(payload.videoId, filtered);
1561
+ this.replies.delete(payload.commentId);
1459
1562
  }
1460
1563
  }
1461
1564
  }
@@ -1986,6 +2089,28 @@ var MockPosterLoader = class {
1986
2089
  // src/preset/adapters/RESTAnalyticsAdapter.ts
1987
2090
  var DEFAULT_BATCH_SIZE = 10;
1988
2091
  var DEFAULT_FLUSH_INTERVAL = 3e4;
2092
+ var DEFAULT_EVENT_TYPE_MAP = {
2093
+ video_view: "view_start",
2094
+ video_complete: "view_end",
2095
+ scroll: "scroll_pass",
2096
+ like: "like",
2097
+ unlike: "like",
2098
+ // Same API type, different action
2099
+ comment: "comment",
2100
+ share: "share",
2101
+ follow: "follow_creator",
2102
+ unfollow: "follow_creator",
2103
+ // Same API type, different action
2104
+ impression: "view_start"
2105
+ };
2106
+ var DEFAULT_CONTEXT = {
2107
+ getSessionId: () => null,
2108
+ getDeviceType: () => "web",
2109
+ getNetworkType: () => "other",
2110
+ getGeoLocation: () => null,
2111
+ getPositionIndex: () => 0,
2112
+ getScrollSpeed: () => null
2113
+ };
1989
2114
  var RESTAnalyticsAdapter = class {
1990
2115
  constructor(config) {
1991
2116
  // Event queue
@@ -1995,12 +2120,19 @@ var RESTAnalyticsAdapter = class {
1995
2120
  // User context
1996
2121
  this.userId = null;
1997
2122
  this.userProperties = {};
2123
+ // Dedupe: track which videos have been viewed (to avoid duplicate view_start)
2124
+ this.viewedVideos = /* @__PURE__ */ new Set();
1998
2125
  this.httpClient = config.httpClient;
1999
2126
  this.endpoint = config.endpoints.batch;
2000
- this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
2001
2127
  this.logger = config.logger;
2002
- const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
2003
- this.flushTimer = setInterval(() => this.flush(), interval);
2128
+ const batchConfig = config.config;
2129
+ this.batchSize = batchConfig?.batchSize ?? config.batchSize ?? DEFAULT_BATCH_SIZE;
2130
+ const flushInterval = batchConfig?.flushInterval ?? config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
2131
+ this.transform = batchConfig?.transform;
2132
+ this.context = batchConfig?.context ?? DEFAULT_CONTEXT;
2133
+ this.eventTypeMap = batchConfig?.eventTypeMap ?? DEFAULT_EVENT_TYPE_MAP;
2134
+ this.useSendBeacon = batchConfig?.useSendBeacon ?? true;
2135
+ this.flushTimer = setInterval(() => this.flush(), flushInterval);
2004
2136
  }
2005
2137
  /**
2006
2138
  * Track an analytics event
@@ -2032,19 +2164,14 @@ var RESTAnalyticsAdapter = class {
2032
2164
  const events = [...this.queue];
2033
2165
  this.queue = [];
2034
2166
  try {
2035
- if (this.trySendBeacon(events)) {
2036
- this.logger?.debug("[RESTAnalyticsAdapter] Events sent via sendBeacon", {
2037
- count: events.length
2038
- });
2167
+ const requestBody = await this.buildRequestBody(events);
2168
+ if (this.trySendBeacon(requestBody)) {
2039
2169
  return;
2040
2170
  }
2041
2171
  await this.httpClient.request({
2042
2172
  method: "POST",
2043
2173
  path: this.endpoint,
2044
- body: { events }
2045
- });
2046
- this.logger?.debug("[RESTAnalyticsAdapter] Events sent via HTTP", {
2047
- count: events.length
2174
+ body: requestBody
2048
2175
  });
2049
2176
  } catch (error) {
2050
2177
  if (this.queue.length < this.batchSize * 3) {
@@ -2054,9 +2181,48 @@ var RESTAnalyticsAdapter = class {
2054
2181
  }
2055
2182
  }
2056
2183
  /**
2057
- * Track video view duration (heartbeat)
2184
+ * Build request body from events
2185
+ * Uses custom transform if provided, otherwise SDK default format
2186
+ */
2187
+ async buildRequestBody(events) {
2188
+ if (!this.transform) {
2189
+ return { events };
2190
+ }
2191
+ const transformedEvents = [];
2192
+ for (const event of events) {
2193
+ try {
2194
+ const eventData = {
2195
+ sdkEventType: event.type,
2196
+ videoId: event.videoId,
2197
+ timestamp: event.timestamp,
2198
+ userId: this.userId,
2199
+ userProperties: this.userProperties,
2200
+ data: event.data
2201
+ };
2202
+ const transformed = await this.transform(eventData, this.context);
2203
+ transformedEvents.push(transformed);
2204
+ } catch (error) {
2205
+ this.logger?.warn("[RESTAnalyticsAdapter] Transform failed for event", {
2206
+ event,
2207
+ error
2208
+ });
2209
+ }
2210
+ }
2211
+ return { events: transformedEvents };
2212
+ }
2213
+ /**
2214
+ * Track video view duration
2215
+ *
2216
+ * Note: PlayerEngine calls this every second (heartbeat), but for batch analytics
2217
+ * we only need ONE view_start per video. This method deduplicates by videoId.
2218
+ *
2219
+ * If you need heartbeat tracking, use RESTViewTrackingAdapter instead.
2058
2220
  */
2059
2221
  trackViewDuration(videoId, duration, totalDuration) {
2222
+ if (this.viewedVideos.has(videoId)) {
2223
+ return;
2224
+ }
2225
+ this.viewedVideos.add(videoId);
2060
2226
  this.track({
2061
2227
  type: "video_view",
2062
2228
  videoId,
@@ -2068,6 +2234,12 @@ var RESTAnalyticsAdapter = class {
2068
2234
  }
2069
2235
  });
2070
2236
  }
2237
+ /**
2238
+ * Clear viewed videos cache (useful for testing or session reset)
2239
+ */
2240
+ clearViewedVideos() {
2241
+ this.viewedVideos.clear();
2242
+ }
2071
2243
  /**
2072
2244
  * Track video completion
2073
2245
  */
@@ -2103,19 +2275,26 @@ var RESTAnalyticsAdapter = class {
2103
2275
  clearInterval(this.flushTimer);
2104
2276
  this.flushTimer = null;
2105
2277
  }
2278
+ this.viewedVideos.clear();
2106
2279
  this.flush().catch(() => {
2107
2280
  });
2108
2281
  }
2109
2282
  /**
2110
2283
  * Try to send via sendBeacon (for reliability on page unload)
2284
+ * Note: sendBeacon shows as "ping" type in Network tab, not "POST"
2285
+ *
2286
+ * Set useSendBeacon: false in config to always use HTTP POST
2111
2287
  */
2112
- trySendBeacon(events) {
2288
+ trySendBeacon(requestBody) {
2289
+ if (this.useSendBeacon === false) {
2290
+ return false;
2291
+ }
2113
2292
  if (typeof navigator === "undefined" || !navigator.sendBeacon) {
2114
2293
  return false;
2115
2294
  }
2116
2295
  try {
2117
2296
  const url = this.buildFullUrl();
2118
- const data = JSON.stringify({ events });
2297
+ const data = JSON.stringify(requestBody);
2119
2298
  const blob = new Blob([data], { type: "application/json" });
2120
2299
  return navigator.sendBeacon(url, blob);
2121
2300
  } catch {
@@ -2126,7 +2305,23 @@ var RESTAnalyticsAdapter = class {
2126
2305
  * Build full URL for sendBeacon
2127
2306
  */
2128
2307
  buildFullUrl() {
2129
- return this.endpoint.startsWith("http") ? this.endpoint : `${window.location.origin}${this.endpoint}`;
2308
+ if (this.endpoint.startsWith("http")) {
2309
+ return this.endpoint;
2310
+ }
2311
+ const baseUrl = this.httpClient.getBaseUrl();
2312
+ return `${baseUrl}${this.endpoint}`;
2313
+ }
2314
+ /**
2315
+ * Get mapped API event type from SDK event type
2316
+ */
2317
+ getApiEventType(sdkEventType) {
2318
+ return this.eventTypeMap[sdkEventType] ?? sdkEventType;
2319
+ }
2320
+ /**
2321
+ * Get current context (for debugging/testing)
2322
+ */
2323
+ getContext() {
2324
+ return this.context;
2130
2325
  }
2131
2326
  };
2132
2327
  function createNoOpAnalyticsAdapter() {
@@ -2260,13 +2455,17 @@ var RESTCommentAdapter = class {
2260
2455
  }
2261
2456
  /**
2262
2457
  * Delete a comment or reply
2458
+ * DELETE /reels/:id/comments/:commentId
2263
2459
  */
2264
2460
  async deleteComment(payload) {
2265
2461
  try {
2266
2462
  await this.httpClient.request({
2267
2463
  method: "DELETE",
2268
2464
  path: this.endpoints.delete,
2269
- pathParams: { id: payload.id }
2465
+ pathParams: {
2466
+ id: payload.videoId,
2467
+ commentId: payload.commentId
2468
+ }
2270
2469
  });
2271
2470
  } catch (error) {
2272
2471
  this.logger?.error("[RESTCommentAdapter] deleteComment failed", error);
@@ -2457,10 +2656,10 @@ var RESTDataAdapter = class {
2457
2656
  const feedData = this.transforms.feedResponse(response);
2458
2657
  const items = feedData.items.map((item) => {
2459
2658
  try {
2460
- return this.transforms.videoItem(item);
2659
+ return this.transforms.contentItem(item);
2461
2660
  } catch (error) {
2462
- this.logger?.error("[RESTDataAdapter] Failed to transform video item", error);
2463
- return this.createFallbackVideoItem(item);
2661
+ this.logger?.error("[RESTDataAdapter] Failed to transform content item", error);
2662
+ return this.createFallbackItem(item);
2464
2663
  }
2465
2664
  });
2466
2665
  return {
@@ -2474,24 +2673,30 @@ var RESTDataAdapter = class {
2474
2673
  }
2475
2674
  }
2476
2675
  /**
2477
- * Get video detail by ID
2676
+ * Get content detail by ID
2478
2677
  */
2479
- async getVideoDetail(id) {
2678
+ async getContentDetail(id) {
2480
2679
  try {
2481
2680
  const response = await this.httpClient.request({
2482
2681
  method: "GET",
2483
2682
  path: this.endpoints.detail,
2484
2683
  pathParams: { id }
2485
2684
  });
2486
- const videoData = this.unwrapResponse(response);
2487
- return this.transforms.videoItem(videoData);
2685
+ const itemData = this.unwrapResponse(response);
2686
+ return this.transforms.contentItem(itemData);
2488
2687
  } catch (error) {
2489
- this.logger?.error("[RESTDataAdapter] getVideoDetail failed", error);
2688
+ this.logger?.error("[RESTDataAdapter] getContentDetail failed", error);
2490
2689
  throw error;
2491
2690
  }
2492
2691
  }
2493
2692
  /**
2494
- * Optional: Prefetch videos
2693
+ * @deprecated Use getContentDetail instead
2694
+ */
2695
+ async getVideoDetail(id) {
2696
+ return this.getContentDetail(id);
2697
+ }
2698
+ /**
2699
+ * Optional: Prefetch content
2495
2700
  * This is a no-op by default, can be overridden if API supports batch fetch
2496
2701
  */
2497
2702
  async prefetch(ids) {
@@ -2512,12 +2717,27 @@ var RESTDataAdapter = class {
2512
2717
  return response;
2513
2718
  }
2514
2719
  /**
2515
- * Create fallback video item when transform fails
2720
+ * Create fallback content item when transform fails
2516
2721
  */
2517
- createFallbackVideoItem(data) {
2722
+ createFallbackItem(data) {
2518
2723
  const obj = data ?? {};
2724
+ const id = String(obj.id ?? obj.video_id ?? obj.article_id ?? `fallback-${Date.now()}`);
2725
+ if (obj.type === "article" || obj.images || obj.photos) {
2726
+ return {
2727
+ type: "article",
2728
+ id,
2729
+ images: [],
2730
+ caption: "",
2731
+ author: { id: "unknown", name: "Unknown" },
2732
+ stats: { likes: 0, comments: 0, shares: 0, views: 0 },
2733
+ isLiked: false,
2734
+ isFollowing: false,
2735
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2736
+ };
2737
+ }
2519
2738
  return {
2520
- id: String(obj.id ?? obj.video_id ?? `fallback-${Date.now()}`),
2739
+ type: "video",
2740
+ id,
2521
2741
  source: {
2522
2742
  url: String(obj.video_url ?? obj.url ?? ""),
2523
2743
  type: "mp4"
@@ -2531,7 +2751,8 @@ var RESTDataAdapter = class {
2531
2751
  views: 0,
2532
2752
  likes: 0,
2533
2753
  comments: 0,
2534
- shares: 0
2754
+ shares: 0,
2755
+ bookmarks: 0
2535
2756
  },
2536
2757
  isLiked: false,
2537
2758
  isFollowing: false,
@@ -2546,6 +2767,8 @@ var RESTInteractionAdapter = class {
2546
2767
  this.httpClient = config.httpClient;
2547
2768
  this.endpoints = config.endpoints;
2548
2769
  this.logger = config.logger;
2770
+ this.customTransformReportReasons = config.transformReportReasons;
2771
+ this.customTransformReportBody = config.transformReportBody;
2549
2772
  }
2550
2773
  /**
2551
2774
  * Like a video
@@ -2670,6 +2893,107 @@ var RESTInteractionAdapter = class {
2670
2893
  this.logger?.warn("[RESTInteractionAdapter] share tracking failed", { error });
2671
2894
  }
2672
2895
  }
2896
+ /**
2897
+ * Report content (video or image post)
2898
+ *
2899
+ * @param contentId - ID of the content to report
2900
+ * @param reason - Report reason code/ID
2901
+ * @param description - Optional additional description
2902
+ */
2903
+ async report(contentId, reason, description) {
2904
+ if (!this.endpoints.report) {
2905
+ this.logger?.warn("[RESTInteractionAdapter] report endpoint not configured");
2906
+ return;
2907
+ }
2908
+ try {
2909
+ const body = this.customTransformReportBody ? this.customTransformReportBody({ contentId, reasonId: reason, description }) : { reason, description };
2910
+ this.logger?.debug("[RESTInteractionAdapter] Sending report", {
2911
+ path: this.endpoints.report,
2912
+ contentId,
2913
+ body
2914
+ });
2915
+ await this.httpClient.request({
2916
+ method: "POST",
2917
+ path: this.endpoints.report,
2918
+ pathParams: { id: contentId },
2919
+ body
2920
+ });
2921
+ this.logger?.debug("[RESTInteractionAdapter] Report sent successfully");
2922
+ } catch (error) {
2923
+ this.logger?.error("[RESTInteractionAdapter] report failed", error);
2924
+ throw error;
2925
+ }
2926
+ }
2927
+ /**
2928
+ * Get available report reasons
2929
+ *
2930
+ * @returns Array of report reasons, or empty array if not configured
2931
+ */
2932
+ async getReportReasons() {
2933
+ if (!this.endpoints.reportReasons) {
2934
+ this.logger?.debug("[RESTInteractionAdapter] reportReasons endpoint not configured");
2935
+ return [];
2936
+ }
2937
+ try {
2938
+ const response = await this.httpClient.request({
2939
+ method: "GET",
2940
+ path: this.endpoints.reportReasons
2941
+ });
2942
+ if (this.customTransformReportReasons) {
2943
+ return this.customTransformReportReasons(response);
2944
+ }
2945
+ return this.transformReportReasons(response);
2946
+ } catch (error) {
2947
+ this.logger?.error("[RESTInteractionAdapter] getReportReasons failed", error);
2948
+ return [];
2949
+ }
2950
+ }
2951
+ /**
2952
+ * Mark content as "not interested"
2953
+ *
2954
+ * Used for recommendation algorithm feedback.
2955
+ * Content should be hidden from feed after this action.
2956
+ *
2957
+ * @param contentId - ID of the content (video or image post)
2958
+ */
2959
+ async notInterested(contentId) {
2960
+ if (!this.endpoints.notInterested) {
2961
+ this.logger?.warn("[RESTInteractionAdapter] notInterested endpoint not configured");
2962
+ return;
2963
+ }
2964
+ try {
2965
+ await this.httpClient.request({
2966
+ method: "POST",
2967
+ path: this.endpoints.notInterested,
2968
+ pathParams: { id: contentId }
2969
+ });
2970
+ } catch (error) {
2971
+ this.logger?.error("[RESTInteractionAdapter] notInterested failed", error);
2972
+ throw error;
2973
+ }
2974
+ }
2975
+ /**
2976
+ * Default transform for API report reasons response to ReportReason[]
2977
+ *
2978
+ * Expected format: [{ id, label, description }]
2979
+ * Or wrapped: { data: [{ id, label, description }] }
2980
+ *
2981
+ * For custom API formats, use `transforms.reportReasons` in preset config.
2982
+ */
2983
+ transformReportReasons(response) {
2984
+ const data = this.unwrapResponse(response);
2985
+ if (!Array.isArray(data)) {
2986
+ return [];
2987
+ }
2988
+ return data.map((item) => {
2989
+ const obj = item;
2990
+ return {
2991
+ id: String(obj.id ?? obj.reason_id ?? ""),
2992
+ label: String(obj.label ?? obj.name ?? obj.title ?? ""),
2993
+ description: obj.description
2994
+ };
2995
+ });
2996
+ }
2673
2997
  /**
2674
2998
  * Transform API comment response to Comment type
2675
2999
  */
@@ -2704,6 +3028,69 @@ var RESTInteractionAdapter = class {
2704
3028
  }
2705
3029
  };
2706
3030
 
3031
+ // src/preset/adapters/RESTPlaylistAdapter.ts
3032
+ var RESTPlaylistAdapter = class {
3033
+ constructor(config) {
3034
+ this.httpClient = config.httpClient;
3035
+ this.endpoint = config.endpoint;
3036
+ this.collectionEndpoint = config.collectionEndpoint;
3037
+ this.transforms = config.transforms;
3038
+ this.logger = config.logger;
3039
+ }
3040
+ /**
3041
+ * Fetch a complete playlist by ID
3042
+ *
3043
+ * @param id - The playlist ID
3044
+ * @returns Promise resolving to PlaylistData
3045
+ */
3046
+ async fetchPlaylist(id) {
3047
+ try {
3048
+ this.logger?.info(`[RESTPlaylistAdapter] Fetching playlist: ${id}`);
3049
+ const path = this.endpoint.replace(":id", id);
3050
+ const response = await this.httpClient.request({
3051
+ method: "GET",
3052
+ path
3053
+ });
3054
+ const playlist = this.transforms.playlist(response);
3055
+ if (playlist.items.length === 0) {
3056
+ this.logger?.warn(`[RESTPlaylistAdapter] Playlist ${id} is empty`);
3057
+ }
3058
+ return playlist;
3059
+ } catch (error) {
3060
+ this.logger?.error(`[RESTPlaylistAdapter] Failed to fetch playlist: ${id}`, error);
3061
+ throw error;
3062
+ }
3063
+ }
3064
+ /**
3065
+ * Fetch a collection of playlists
3066
+ *
3067
+ * @param cursor - Pagination cursor
3068
+ * @returns Promise resolving to PlaylistCollectionResponse
3069
+ */
3070
+ async fetchPlaylistCollection(cursor) {
3071
+ try {
3072
+ if (!this.collectionEndpoint || !this.transforms.collection) {
3073
+ throw new Error(
3074
+ "[RESTPlaylistAdapter] collectionEndpoint or collection transform not configured"
3075
+ );
3076
+ }
3077
+ this.logger?.info(`[RESTPlaylistAdapter] Fetching playlist collection (cursor: ${cursor})`);
3078
+ const path = cursor ? `${this.collectionEndpoint}?cursor=${cursor}` : this.collectionEndpoint;
3079
+ const response = await this.httpClient.request({
3080
+ method: "GET",
3081
+ path
3082
+ });
3083
+ return this.transforms.collection(response);
3084
+ } catch (error) {
3085
+ this.logger?.error(
3086
+ "[RESTPlaylistAdapter] Failed to fetch playlist collection",
3087
+ error
3088
+ );
3089
+ throw error;
3090
+ }
3091
+ }
3092
+ };
3093
+
2707
3094
  // src/preset/adapters/RESTViewTrackingAdapter.ts
2708
3095
  var DEFAULT_VIEW_EVENT_VALUE = "seek";
2709
3096
  var DEFAULT_HEARTBEAT_INTERVAL = 1e4;
@@ -2958,6 +3345,12 @@ var HttpClient = class {
2958
3345
  };
2959
3346
  this.retryConfig = mergedRetry;
2960
3347
  }
3348
+ /**
3349
+ * Get base URL (for sendBeacon which needs full URL)
3350
+ */
3351
+ getBaseUrl() {
3352
+ return this.config.baseUrl;
3353
+ }
2961
3354
  /**
2962
3355
  * Make HTTP request with auth and retry
2963
3356
  */
@@ -3168,6 +3561,83 @@ var HttpClient = class {
3168
3561
  }
3169
3562
  };
3170
3563
 
3564
+ // src/preset/transforms/playlist.ts
3565
+ function defaultPlaylistTransform(apiResponse, contentItemTransform, logger) {
3566
+ if (!apiResponse || typeof apiResponse !== "object") {
3567
+ logger?.error("[PlaylistTransform] Invalid API response", void 0, {
3568
+ apiResponse: String(apiResponse)
3569
+ });
3570
+ return createEmptyPlaylist();
3571
+ }
3572
+ const obj = apiResponse;
3573
+ const data = obj.data ?? obj.result ?? obj.playlist ?? obj;
3574
+ const rawItems = data.items ?? data.reels ?? data.videos ?? data.list ?? [];
3575
+ if (!Array.isArray(rawItems)) {
3576
+ logger?.warn("[PlaylistTransform] Items is not an array", { data });
3577
+ return createEmptyPlaylist(data);
3578
+ }
3579
+ const items = rawItems.map((item) => {
3580
+ try {
3581
+ return contentItemTransform(item);
3582
+ } catch (error) {
3583
+ logger?.error("[PlaylistTransform] Failed to transform item", error);
3584
+ return null;
3585
+ }
3586
+ }).filter((item) => item !== null);
3587
+ return {
3588
+ id: String(data.id ?? data.playlist_id ?? ""),
3589
+ title: String(data.title ?? data.name ?? "Untitled Playlist"),
3590
+ description: String(data.description ?? ""),
3591
+ cover: String(data.cover ?? data.cover_url ?? data.thumbnail ?? ""),
3592
+ items,
3593
+ totalItems: Number(data.total_items ?? data.total ?? items.length)
3594
+ };
3595
+ }
3596
+ function defaultPlaylistSummaryTransform(data) {
3597
+ const obj = data;
3598
+ const authorObj = obj.author ?? obj.user ?? obj.creator ?? {};
3599
+ return {
3600
+ id: String(obj.id ?? obj.playlist_id ?? ""),
3601
+ title: String(obj.title ?? obj.name ?? "Untitled Playlist"),
3602
+ description: String(obj.description ?? ""),
3603
+ cover: String(obj.cover ?? obj.cover_url ?? obj.thumbnail ?? ""),
3604
+ totalItems: Number(obj.total_items ?? obj.items_count ?? 0),
3605
+ author: {
3606
+ id: String(authorObj.id ?? authorObj.user_id ?? ""),
3607
+ name: String(authorObj.name ?? authorObj.display_name ?? authorObj.username ?? "Unknown")
3608
+ },
3609
+ updatedAt: String(obj.updated_at ?? obj.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString())
3610
+ };
3611
+ }
3612
+ function defaultPlaylistCollectionTransform(apiResponse, logger) {
3613
+ if (!apiResponse || typeof apiResponse !== "object") {
3614
+ return { playlists: [], nextCursor: null, hasMore: false };
3615
+ }
3616
+ const obj = apiResponse;
3617
+ const data = obj.data ?? obj.result ?? obj;
3618
+ const rawPlaylists = data.playlists ?? data.items ?? data.list ?? [];
3619
+ if (!Array.isArray(rawPlaylists)) {
3620
+ logger?.warn("[PlaylistTransform] Playlists is not an array", { data });
3621
+ return { playlists: [], nextCursor: null, hasMore: false };
3622
+ }
3623
+ const playlists = rawPlaylists.map(defaultPlaylistSummaryTransform);
3624
+ const nextCursor = String(data.next_cursor ?? data.cursor ?? data.nextCursor ?? null);
3625
+ const hasMore = Boolean(data.has_more ?? data.hasMore ?? (nextCursor && nextCursor !== "null"));
3626
+ return {
3627
+ playlists,
3628
+ nextCursor: nextCursor === "null" ? null : nextCursor,
3629
+ hasMore
3630
+ };
3631
+ }
3632
+ function createEmptyPlaylist(data) {
3633
+ return {
3634
+ id: String(data?.id ?? ""),
3635
+ title: String(data?.title ?? "Empty Playlist"),
3636
+ items: [],
3637
+ totalItems: 0
3638
+ };
3639
+ }
3640
+
3171
3641
  // src/preset/transforms/defaults.ts
3172
3642
  function getNestedValue(obj, path) {
3173
3643
  if (!obj || typeof obj !== "object") return void 0;
@@ -3230,9 +3700,11 @@ function defaultAuthorTransform(data) {
3230
3700
  return {
3231
3701
  id: toSafeString(tryFields(author, "id", "user_id", "author_id"), ""),
3232
3702
  name: toSafeString(
3233
- tryFields(author, "display_name", "name", "username", "nickname"),
3703
+ tryFields(author, "display_name", "name", "username", "nickname", "first_name"),
3234
3704
  "Unknown"
3235
3705
  ),
3706
+ // Combine first/last name if present and name is default
3707
+ // Note: This is a simple fallback, specialized logic should be in a custom transform if needed
3236
3708
  avatar: toSafeString(
3237
3709
  tryFields(author, "avatar", "avatar_url", "profile_picture", "photo"),
3238
3710
  void 0
@@ -3248,7 +3720,8 @@ function defaultStatsTransform(data) {
3248
3720
  views: toNumber(tryFields(stats, "view_count", "views", "play_count"), 0),
3249
3721
  likes: toNumber(tryFields(stats, "like_count", "likes", "digg_count"), 0),
3250
3722
  comments: toNumber(tryFields(stats, "comment_count", "comments"), 0),
3251
- shares: toNumber(tryFields(stats, "share_count", "shares"), 0)
3723
+ shares: toNumber(tryFields(stats, "share_count", "shares"), 0),
3724
+ bookmarks: toNumber(tryFields(stats, "bookmark_count", "bookmarks", "saves"), 0)
3252
3725
  };
3253
3726
  }
3254
3727
  function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
@@ -3275,6 +3748,7 @@ function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
3275
3748
  const author = defaultAuthorTransform(obj);
3276
3749
  const stats = defaultStatsTransform(obj);
3277
3750
  const videoItem = {
3751
+ type: "video",
3278
3752
  id,
3279
3753
  source,
3280
3754
  poster: toSafeString(
@@ -3301,6 +3775,110 @@ function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
3301
3775
  };
3302
3776
  return videoItem;
3303
3777
  }
3778
+ function defaultArticleItemTransform(apiResponse, fieldMap, logger) {
3779
+ const obj = apiResponse;
3780
+ const getMapped = (sdkField, ...fallbacks) => {
3781
+ if (fieldMap?.[sdkField]) {
3782
+ const mapped = getNestedValue(obj, fieldMap[sdkField]);
3783
+ if (mapped !== void 0) return mapped;
3784
+ }
3785
+ return tryFields(obj, ...fallbacks);
3786
+ };
3787
+ const id = toSafeString(getMapped("id", "id", "article_id", "post_id", "_id"), "");
3788
+ if (!id) {
3789
+ logger?.warn("[Transform article] Missing required field: id", { data: obj });
3790
+ }
3791
+ let images = getMapped("images", "images", "media", "photos", "gallery") ?? [];
3792
+ if (!Array.isArray(images) || images.length === 0) {
3793
+ const singleImage = toSafeString(
3794
+ getMapped("image", "image_url", "url", "thumbnail", "cover"),
3795
+ void 0
3796
+ );
3797
+ images = singleImage ? [singleImage] : [];
3798
+ }
3799
+ return {
3800
+ type: "article",
3801
+ id,
3802
+ images,
3803
+ caption: toSafeString(
3804
+ getMapped("caption", "caption", "description", "text", "content", "title", "subject"),
3805
+ ""
3806
+ ),
3807
+ author: defaultAuthorTransform(obj),
3808
+ stats: defaultStatsTransform(obj),
3809
+ isLiked: toBoolean(getMapped("isLiked", "is_liked", "liked", "user_liked"), false),
3810
+ isFollowing: toBoolean(getMapped("isFollowing", "is_following", "following"), false),
3811
+ createdAt: toSafeString(
3812
+ getMapped("createdAt", "created_at", "create_time", "timestamp"),
3813
+ (/* @__PURE__ */ new Date()).toISOString()
3814
+ ),
3815
+ hashtags: getMapped("hashtags", "hashtags", "tags", "hash_tags") ?? []
3816
+ };
3817
+ }
3818
+ function extractMediaInfo(obj) {
3819
+ const mediaList = tryFields(obj, "media", "medias") ?? [];
3820
+ let videoMedia;
3821
+ let imageMedias = [];
3822
+ if (Array.isArray(mediaList) && mediaList.length > 0) {
3823
+ videoMedia = mediaList.find((m) => m.type === "video");
3824
+ imageMedias = mediaList.filter((m) => m.type === "image");
3825
+ }
3826
+ return { videoMedia, imageMedias };
3827
+ }
3828
+ function extractMusicInfo(obj) {
3829
+ const soundObj = tryFields(obj, "sound", "music", "audio");
3830
+ if (soundObj && (soundObj.id || soundObj.url || soundObj.name || soundObj.title)) {
3831
+ return {
3832
+ id: toSafeString(tryFields(soundObj, "id", "uuid"), "unknown"),
3833
+ title: toSafeString(tryFields(soundObj, "title", "name", "song"), "Unknown Sound"),
3834
+ artist: toSafeString(tryFields(soundObj, "artist", "author", "singer"), "Unknown Artist"),
3835
+ cover: toSafeString(tryFields(soundObj, "cover", "poster", "thumbnail", "image"), void 0),
3836
+ audioUrl: toSafeString(tryFields(soundObj, "url", "audio_url", "download_url"), void 0)
3837
+ };
3838
+ }
3839
+ return void 0;
3840
+ }
3841
+ function defaultContentItemTransform(apiResponse, fieldMap, logger) {
3842
+ const obj = apiResponse;
3843
+ const { videoMedia, imageMedias } = extractMediaInfo(obj);
3844
+ const musicInfo = extractMusicInfo(obj);
3845
+ const typeField = toSafeString(tryFields(obj, "type", "content_type", "item_type")).toLowerCase();
3846
+ let isVideo = false;
3847
+ if (["article", "image", "post", "photo"].includes(typeField)) {
3848
+ isVideo = false;
3849
+ } else if (["video", "reel", "short"].includes(typeField)) {
3850
+ isVideo = true;
3851
+ } else {
3852
+ if (videoMedia || tryFields(obj, "video_url", "playback_url", "source")) {
3853
+ isVideo = true;
3854
+ } else if (imageMedias.length > 0 || tryFields(obj, "images", "photos", "gallery")) {
3855
+ isVideo = false;
3856
+ } else {
3857
+ isVideo = true;
3858
+ }
3859
+ }
3860
+ if (isVideo) {
3861
+ if (videoMedia) {
3862
+ Object.assign(obj, {
3863
+ video_url: videoMedia.url || videoMedia.download_url,
3864
+ poster: videoMedia.poster || videoMedia.thumbnail,
3865
+ duration: videoMedia.duration
3866
+ });
3867
+ }
3868
+ const item2 = defaultVideoItemTransform(apiResponse, fieldMap?.video, logger);
3869
+ if (musicInfo) item2.music = musicInfo;
3870
+ return item2;
3871
+ }
3872
+ if (imageMedias.length > 0) {
3873
+ const imageUrls = imageMedias.map((m) => toSafeString(m.url || m.download_url)).filter(Boolean);
3874
+ if (imageUrls.length > 0) {
3875
+ obj.images = imageUrls;
3876
+ }
3877
+ }
3878
+ const item = defaultArticleItemTransform(apiResponse, fieldMap?.article, logger);
3879
+ if (musicInfo) item.music = musicInfo;
3880
+ return item;
3881
+ }
3304
3882
  function defaultFeedResponseTransform(apiResponse, fieldMap, logger) {
3305
3883
  const obj = apiResponse;
3306
3884
  const itemsPath = fieldMap?.items;
@@ -3354,9 +3932,22 @@ function defaultFeedResponseTransform(apiResponse, fieldMap, logger) {
3354
3932
  return { items, nextCursor, hasMore };
3355
3933
  }
3356
3934
  function createTransforms(config, logger) {
3935
+ const contentItem = (data) => {
3936
+ if (config?.contentItem) return config.contentItem(data);
3937
+ if (config?.videoItem) return config.videoItem(data);
3938
+ return defaultContentItemTransform(data, config?.fieldMap, logger);
3939
+ };
3357
3940
  return {
3358
- videoItem: config?.videoItem ? config.videoItem : (data) => defaultVideoItemTransform(data, config?.fieldMap?.video, logger),
3359
- feedResponse: config?.feedResponse ? config.feedResponse : (data) => defaultFeedResponseTransform(data, config?.fieldMap?.feed, logger)
3941
+ contentItem,
3942
+ videoItem: (data) => contentItem(data),
3943
+ feedResponse: config?.feedResponse ? config.feedResponse : (data) => defaultFeedResponseTransform(data, config?.fieldMap?.feed, logger),
3944
+ playlist: config?.playlist ? config.playlist : (data) => defaultPlaylistTransform(
3945
+ data,
3946
+ (item) => contentItem(item),
3947
+ // Pass the generic contentItem transform
3948
+ logger
3949
+ ),
3950
+ playlistCollection: config?.playlistCollection ? config.playlistCollection : (data) => defaultPlaylistCollectionTransform(data, logger)
3360
3951
  };
3361
3952
  }
3362
3953
 
@@ -3408,7 +3999,9 @@ function createRESTAdapters(config) {
3408
3999
  const interaction = new RESTInteractionAdapter({
3409
4000
  httpClient,
3410
4001
  endpoints: endpoints.interaction,
3411
- logger
4002
+ logger,
4003
+ transformReportReasons: transforms?.reportReasons,
4004
+ transformReportBody: transforms?.reportBody
3412
4005
  });
3413
4006
  let analytics;
3414
4007
  if (endpoints.viewTracking) {
@@ -3422,6 +4015,7 @@ function createRESTAdapters(config) {
3422
4015
  analytics = new RESTAnalyticsAdapter({
3423
4016
  httpClient,
3424
4017
  endpoints: endpoints.analytics,
4018
+ config: config.batchAnalytics,
3425
4019
  logger
3426
4020
  });
3427
4021
  } else {
@@ -3432,11 +4026,22 @@ function createRESTAdapters(config) {
3432
4026
  endpoints: endpoints.comment,
3433
4027
  logger
3434
4028
  }) : void 0;
4029
+ const playlist = endpoints.playlist ? new RESTPlaylistAdapter({
4030
+ httpClient,
4031
+ endpoint: endpoints.playlist.detail,
4032
+ collectionEndpoint: endpoints.playlist.list,
4033
+ transforms: {
4034
+ playlist: resolvedTransforms.playlist,
4035
+ collection: (data) => resolvedTransforms.playlistCollection(data)
4036
+ },
4037
+ logger
4038
+ }) : void 0;
3435
4039
  return {
3436
4040
  dataSource,
3437
4041
  interaction,
3438
4042
  analytics,
3439
- comment
4043
+ comment,
4044
+ playlist
3440
4045
  };
3441
4046
  }
3442
4047
 
@@ -3807,6 +4412,25 @@ var LocalStorageAdapter = class {
3807
4412
  count: keysToRemove.length
3808
4413
  });
3809
4414
  }
4415
+ /**
4416
+ * Get a value from localStorage synchronously
4417
+ *
4418
+ * Used for zero-flash cache hydration during React render phase.
4419
+ * localStorage is inherently synchronous, so this bypasses the async wrapper.
4420
+ */
4421
+ getSync(key) {
4422
+ try {
4423
+ const fullKey = this.buildKey(key);
4424
+ const value = localStorage.getItem(fullKey);
4425
+ if (value === null) {
4426
+ return null;
4427
+ }
4428
+ return JSON.parse(value);
4429
+ } catch (error) {
4430
+ this.logger?.warn("[LocalStorageAdapter] Failed to getSync/parse value", { key, error });
4431
+ return null;
4432
+ }
4433
+ }
3810
4434
  /**
3811
4435
  * Get all SDK-namespaced keys
3812
4436
  */
@@ -4071,8 +4695,9 @@ function createBrowserAdapters(config) {
4071
4695
  videoLoader,
4072
4696
  posterLoader,
4073
4697
  comment,
4698
+ playlist: restAdapters.playlist,
4074
4699
  logger: config.logger
4075
4700
  };
4076
4701
  }
4077
4702
 
4078
- export { BrowserPosterLoader, BrowserVideoLoader, DEFAULT_REQUEST_CONFIG, DEFAULT_RETRY_CONFIG, HttpClient, HttpError, LocalSessionStorageAdapter, LocalStorageAdapter, MockAnalyticsAdapter, MockCommentAdapter, MockDataAdapter, MockInteractionAdapter, MockLoggerAdapter, MockNetworkAdapter, MockPosterLoader, MockSessionStorageAdapter, MockStorageAdapter, MockVideoLoader, RESTAnalyticsAdapter, RESTCommentAdapter, RESTDataAdapter, RESTInteractionAdapter, WebNetworkAdapter, createBrowserAdapters, createBrowserPosterLoader, createBrowserVideoLoader, createLocalStorageAdapter, createNoOpAnalyticsAdapter, createRESTAdapters, createSessionStorageAdapter, createTransforms, createWebNetworkAdapter, defaultFeedResponseTransform, defaultVideoItemTransform };
4703
+ export { BrowserPosterLoader, BrowserVideoLoader, DEFAULT_REQUEST_CONFIG, DEFAULT_RETRY_CONFIG, HttpClient, HttpError, LocalSessionStorageAdapter, LocalStorageAdapter, MockAnalyticsAdapter, MockCommentAdapter, MockDataAdapter, MockInteractionAdapter, MockLoggerAdapter, MockNetworkAdapter, MockPlaylistAdapter, MockPosterLoader, MockSessionStorageAdapter, MockStorageAdapter, MockVideoLoader, RESTAnalyticsAdapter, RESTCommentAdapter, RESTDataAdapter, RESTInteractionAdapter, RESTViewTrackingAdapter, WebNetworkAdapter, createBrowserAdapters, createBrowserPosterLoader, createBrowserVideoLoader, createLocalStorageAdapter, createNoOpAnalyticsAdapter, createRESTAdapters, createSessionStorageAdapter, createTransforms, createWebNetworkAdapter, defaultFeedResponseTransform, defaultVideoItemTransform };