@xhub-short/core 0.1.0-beta.1 → 0.1.0-beta.10

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 CHANGED
@@ -1,6 +1,26 @@
1
- import { createStore } from 'zustand/vanilla';
2
-
3
- // src/feed/FeedManager.ts
1
+ // ../../node_modules/.pnpm/zustand@5.0.9_@types+react@19.2.7_react@19.2.3/node_modules/zustand/esm/vanilla.mjs
2
+ var createStoreImpl = (createState) => {
3
+ let state;
4
+ const listeners = /* @__PURE__ */ new Set();
5
+ const setState = (partial, replace) => {
6
+ const nextState = typeof partial === "function" ? partial(state) : partial;
7
+ if (!Object.is(nextState, state)) {
8
+ const previousState = state;
9
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
10
+ listeners.forEach((listener) => listener(state, previousState));
11
+ }
12
+ };
13
+ const getState = () => state;
14
+ const getInitialState = () => initialState;
15
+ const subscribe = (listener) => {
16
+ listeners.add(listener);
17
+ return () => listeners.delete(listener);
18
+ };
19
+ const api = { setState, getState, getInitialState, subscribe };
20
+ const initialState = state = createState(setState, getState, api);
21
+ return api;
22
+ };
23
+ var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
4
24
 
5
25
  // src/feed/types.ts
6
26
  var DEFAULT_FEED_CONFIG = {
@@ -13,6 +33,13 @@ var DEFAULT_FEED_CONFIG = {
13
33
  maxCacheSize: 100,
14
34
  enableGC: true
15
35
  };
36
+ var DEFAULT_PREFETCH_CACHE_CONFIG = {
37
+ enabled: true,
38
+ maxVideos: 10,
39
+ storageKey: "sv-prefetch-cache",
40
+ enableDynamicEviction: true,
41
+ evictionThrottleMs: 2e3
42
+ };
16
43
 
17
44
  // src/feed/FeedManager.ts
18
45
  var createInitialState = () => ({
@@ -26,8 +53,8 @@ var createInitialState = () => ({
26
53
  isStale: false,
27
54
  lastFetchTime: null
28
55
  });
29
- var FeedManager = class {
30
- constructor(dataSource, config = {}) {
56
+ var _FeedManager = class _FeedManager {
57
+ constructor(dataSource, config = {}, storage, prefetchConfig) {
31
58
  this.dataSource = dataSource;
32
59
  /** Abort controller for cancelling in-flight requests */
33
60
  this.abortController = null;
@@ -42,8 +69,46 @@ var FeedManager = class {
42
69
  */
43
70
  this.accessOrder = /* @__PURE__ */ new Map();
44
71
  this.config = { ...DEFAULT_FEED_CONFIG, ...config };
72
+ this.prefetchConfig = { ...DEFAULT_PREFETCH_CACHE_CONFIG, ...prefetchConfig };
73
+ this.storage = storage ?? null;
45
74
  this.store = createStore(createInitialState);
46
75
  }
76
+ /**
77
+ * Prefetch feed data and store in global memory cache
78
+ *
79
+ * @param dataSource - Data source to fetch from
80
+ * @param options - Prefetch options
81
+ */
82
+ static async prefetch(dataSource, options = {}) {
83
+ if (_FeedManager.globalMemoryCache) {
84
+ const now = Date.now();
85
+ const ttl = options.ttl ?? 5 * 60 * 1e3;
86
+ if (now - _FeedManager.globalMemoryCache.timestamp < ttl) {
87
+ return;
88
+ }
89
+ }
90
+ try {
91
+ const response = await dataSource.fetchFeed(void 0);
92
+ _FeedManager.globalMemoryCache = {
93
+ items: response.items,
94
+ nextCursor: response.nextCursor,
95
+ timestamp: Date.now()
96
+ };
97
+ } catch {
98
+ }
99
+ }
100
+ /**
101
+ * Check if prefetch cache exists and is valid
102
+ */
103
+ static hasPrefetchCache() {
104
+ return !!_FeedManager.globalMemoryCache;
105
+ }
106
+ /**
107
+ * Clear prefetch cache
108
+ */
109
+ static clearPrefetchCache() {
110
+ _FeedManager.globalMemoryCache = null;
111
+ }
47
112
  // ═══════════════════════════════════════════════════════════════
48
113
  // PUBLIC API
49
114
  // ═══════════════════════════════════════════════════════════════
@@ -60,6 +125,12 @@ var FeedManager = class {
60
125
  * - Prevents duplicate API calls from rapid UI interactions
61
126
  */
62
127
  async loadInitial() {
128
+ if (_FeedManager.globalMemoryCache) {
129
+ const { items, nextCursor } = _FeedManager.globalMemoryCache;
130
+ this.hydrateFromSnapshot(items, nextCursor, { markAsStale: false });
131
+ _FeedManager.globalMemoryCache = null;
132
+ return;
133
+ }
63
134
  const dedupeKey = "__initial__";
64
135
  const existingRequest = this.inFlightRequests.get(dedupeKey);
65
136
  if (existingRequest) {
@@ -84,6 +155,7 @@ var FeedManager = class {
84
155
  async executeLoadInitial() {
85
156
  try {
86
157
  await this.fetchWithRetry();
158
+ this.updatePrefetchCache();
87
159
  } catch (error) {
88
160
  this.handleError(error, "loadInitial");
89
161
  } finally {
@@ -124,6 +196,7 @@ var FeedManager = class {
124
196
  async executeLoadMore(cursor) {
125
197
  try {
126
198
  await this.fetchWithRetry(cursor);
199
+ this.updatePrefetchCache();
127
200
  } catch (error) {
128
201
  this.handleError(error, "loadMore");
129
202
  } finally {
@@ -252,6 +325,121 @@ var FeedManager = class {
252
325
  });
253
326
  }
254
327
  // ═══════════════════════════════════════════════════════════════
328
+ // PREFETCH CACHE METHODS
329
+ // ═══════════════════════════════════════════════════════════════
330
+ /**
331
+ * Update prefetch cache with current feed tail
332
+ * Called automatically after loadInitial() and loadMore()
333
+ *
334
+ * Strategy: Cache the LAST N videos (tail of feed)
335
+ * These are videos user hasn't seen yet, perfect for instant display
336
+ */
337
+ updatePrefetchCache() {
338
+ if (!this.prefetchConfig.enabled) return;
339
+ if (!this.storage) return;
340
+ const state = this.store.getState();
341
+ const allVideos = this.getVideos();
342
+ if (allVideos.length < this.prefetchConfig.maxVideos) {
343
+ return;
344
+ }
345
+ const tailVideos = allVideos.slice(-this.prefetchConfig.maxVideos);
346
+ const cacheData = {
347
+ items: tailVideos,
348
+ savedAt: Date.now(),
349
+ cursor: state.cursor
350
+ };
351
+ this.savePrefetchCacheAsync(cacheData);
352
+ }
353
+ /**
354
+ * Save prefetch cache to storage (async, non-blocking)
355
+ */
356
+ async savePrefetchCacheAsync(data) {
357
+ if (!this.storage) return;
358
+ try {
359
+ await this.storage.set(this.prefetchConfig.storageKey, data);
360
+ } catch {
361
+ }
362
+ }
363
+ /**
364
+ * Load prefetch cache from storage
365
+ * Returns null if no cache, disabled, or storage error
366
+ */
367
+ async loadPrefetchCache() {
368
+ if (!this.prefetchConfig.enabled) return null;
369
+ if (!this.storage) return null;
370
+ try {
371
+ const data = await this.storage.get(this.prefetchConfig.storageKey);
372
+ if (!data || !data.items || data.items.length === 0) {
373
+ return null;
374
+ }
375
+ return data;
376
+ } catch {
377
+ return null;
378
+ }
379
+ }
380
+ /**
381
+ * Hydrate feed from prefetch cache for instant display
382
+ * Marks data as stale to trigger background revalidation
383
+ */
384
+ hydrateFromPrefetchCache(cache) {
385
+ this.hydrateFromSnapshot(cache.items, cache.cursor, {
386
+ markAsStale: true
387
+ // Trigger revalidation
388
+ });
389
+ }
390
+ /**
391
+ * Clear prefetch cache
392
+ * Call when user logs out or data should be invalidated
393
+ */
394
+ async clearPrefetchCache() {
395
+ if (!this.storage) return;
396
+ try {
397
+ await this.storage.remove(this.prefetchConfig.storageKey);
398
+ } catch {
399
+ }
400
+ }
401
+ /**
402
+ * Evict videos that user has scrolled past from prefetch cache
403
+ * Called when user's focusedIndex changes
404
+ *
405
+ * Strategy: Remove all videos at or before current position
406
+ * This ensures user doesn't rewatch videos on reload
407
+ *
408
+ * @param currentIndex - Current focused video index in feed
409
+ */
410
+ async evictViewedVideosFromCache(currentIndex) {
411
+ if (!this.prefetchConfig.enabled) return;
412
+ if (!this.prefetchConfig.enableDynamicEviction) return;
413
+ if (!this.storage) return;
414
+ try {
415
+ const cache = await this.loadPrefetchCache();
416
+ if (!cache || cache.items.length === 0) return;
417
+ const allVideos = this.getVideos();
418
+ const currentVideo = allVideos[currentIndex];
419
+ if (!currentVideo) return;
420
+ const viewedVideoIds = new Set(allVideos.slice(0, currentIndex + 1).map((v) => v.id));
421
+ const updatedItems = cache.items.filter((item) => !viewedVideoIds.has(item.id));
422
+ if (updatedItems.length === cache.items.length) return;
423
+ if (updatedItems.length === 0) {
424
+ await this.storage.remove(this.prefetchConfig.storageKey);
425
+ return;
426
+ }
427
+ const updatedCache = {
428
+ ...cache,
429
+ items: updatedItems,
430
+ savedAt: Date.now()
431
+ };
432
+ await this.storage.set(this.prefetchConfig.storageKey, updatedCache);
433
+ } catch {
434
+ }
435
+ }
436
+ /**
437
+ * Get prefetch cache configuration (for external access)
438
+ */
439
+ getPrefetchConfig() {
440
+ return this.prefetchConfig;
441
+ }
442
+ // ═══════════════════════════════════════════════════════════════
255
443
  // PRIVATE METHODS
256
444
  // ═══════════════════════════════════════════════════════════════
257
445
  /**
@@ -459,6 +647,12 @@ var FeedManager = class {
459
647
  };
460
648
  }
461
649
  };
650
+ // ═══════════════════════════════════════════════════════════════
651
+ // STATIC MEMORY CACHE (PREFETCH)
652
+ // ═══════════════════════════════════════════════════════════════
653
+ /** Static memory cache for explicit prefetching */
654
+ _FeedManager.globalMemoryCache = null;
655
+ var FeedManager = _FeedManager;
462
656
 
463
657
  // src/player/types.ts
464
658
  var PlayerStatus = /* @__PURE__ */ ((PlayerStatus2) => {
@@ -546,7 +740,8 @@ var createInitialState2 = (config) => ({
546
740
  ended: false,
547
741
  // Session restore
548
742
  pendingRestoreTime: null,
549
- pendingRestoreVideoId: null
743
+ pendingRestoreVideoId: null,
744
+ pendingRestoreFrame: null
550
745
  });
551
746
  var PlayerEngine = class {
552
747
  constructor(config = {}) {
@@ -595,7 +790,11 @@ var PlayerEngine = class {
595
790
  }
596
791
  return false;
597
792
  }
598
- const currentStatus = this.store.getState().status;
793
+ const currentState = this.store.getState();
794
+ const currentStatus = currentState.status;
795
+ if (currentState.currentVideo && currentState.currentVideo.id !== video.id) {
796
+ this.trackLeaveVideo(currentState);
797
+ }
599
798
  if (currentStatus !== "idle" /* IDLE */ && currentStatus !== "error" /* ERROR */ && currentStatus !== "paused" /* PAUSED */) {
600
799
  if (!this.transitionTo("idle" /* IDLE */)) {
601
800
  return false;
@@ -821,22 +1020,24 @@ var PlayerEngine = class {
821
1020
  * ```typescript
822
1021
  * // During session restore
823
1022
  * if (result.playbackTime && result.currentVideoId) {
824
- * playerEngine.setRestorePosition(result.currentVideoId, result.playbackTime);
1023
+ * playerEngine.setRestorePosition(result.currentVideoId, result.playbackTime, result.restoreFrame);
825
1024
  * }
826
1025
  * ```
827
1026
  */
828
- setRestorePosition(videoId, time) {
1027
+ setRestorePosition(videoId, time, frame) {
829
1028
  this.store.setState({
830
1029
  pendingRestoreTime: time,
831
- pendingRestoreVideoId: videoId
1030
+ pendingRestoreVideoId: videoId,
1031
+ pendingRestoreFrame: frame ?? null
832
1032
  });
833
- this.logger?.debug("[PlayerEngine] Set restore position", { videoId, time });
1033
+ this.logger?.debug("[PlayerEngine] Set restore position", { videoId, time, hasFrame: !!frame });
834
1034
  }
835
1035
  /**
836
1036
  * Get and clear restore position for a video.
837
1037
  *
838
1038
  * VideoPlayer calls this when rendering to get startTime.
839
1039
  * Position is cleared after being read to prevent reuse.
1040
+ * Note: restoreFrame is NOT cleared here - use consumeRestoreFrame() separately.
840
1041
  *
841
1042
  * @param videoId - Video ID to check
842
1043
  * @returns Restore time in seconds, or null if no pending restore
@@ -846,14 +1047,51 @@ var PlayerEngine = class {
846
1047
  if (state.pendingRestoreVideoId === videoId && state.pendingRestoreTime !== null) {
847
1048
  const time = state.pendingRestoreTime;
848
1049
  this.store.setState({
849
- pendingRestoreTime: null,
850
- pendingRestoreVideoId: null
1050
+ pendingRestoreTime: null
1051
+ // Note: pendingRestoreVideoId and pendingRestoreFrame kept for frame consumption
851
1052
  });
852
1053
  this.logger?.debug("[PlayerEngine] Consumed restore position", { videoId, time });
853
1054
  return time;
854
1055
  }
855
1056
  return null;
856
1057
  }
1058
+ /**
1059
+ * Get and clear restore frame for a video.
1060
+ *
1061
+ * VideoSlot calls this to display instant preview while video loads.
1062
+ * Frame is cleared after being read to free memory.
1063
+ *
1064
+ * @param videoId - Video ID to check
1065
+ * @returns Base64 JPEG data URL, or null if no pending frame
1066
+ */
1067
+ consumeRestoreFrame(videoId) {
1068
+ const state = this.store.getState();
1069
+ if (state.pendingRestoreVideoId === videoId && state.pendingRestoreFrame !== null) {
1070
+ const frame = state.pendingRestoreFrame;
1071
+ this.store.setState({
1072
+ pendingRestoreFrame: null,
1073
+ pendingRestoreVideoId: null
1074
+ // Clear video ID since we've consumed both time and frame
1075
+ });
1076
+ this.logger?.debug("[PlayerEngine] Consumed restore frame", { videoId });
1077
+ return frame;
1078
+ }
1079
+ return null;
1080
+ }
1081
+ /**
1082
+ * Get restore frame without consuming (peek).
1083
+ * Used to display preview while keeping it available.
1084
+ *
1085
+ * @param videoId - Video ID to check
1086
+ * @returns Base64 JPEG data URL, or null if no pending frame
1087
+ */
1088
+ peekRestoreFrame(videoId) {
1089
+ const state = this.store.getState();
1090
+ if (state.pendingRestoreVideoId === videoId && state.pendingRestoreFrame !== null) {
1091
+ return state.pendingRestoreFrame;
1092
+ }
1093
+ return null;
1094
+ }
857
1095
  /**
858
1096
  * Check if there's a pending restore position for a video.
859
1097
  *
@@ -964,13 +1202,23 @@ var PlayerEngine = class {
964
1202
  }
965
1203
  /**
966
1204
  * Start watch time tracking
1205
+ * Increments watchTime every second and sends analytics heartbeat
967
1206
  */
968
1207
  startWatchTimeTracking() {
969
1208
  if (this.watchTimeInterval) return;
970
1209
  this.watchTimeInterval = setInterval(() => {
971
1210
  const state = this.store.getState();
972
- if (state.status === "playing" /* PLAYING */) {
973
- this.store.setState({ watchTime: state.watchTime + 1 });
1211
+ if (state.status === "playing" /* PLAYING */ && state.currentVideo) {
1212
+ const newWatchTime = state.watchTime + 1;
1213
+ this.store.setState({ watchTime: newWatchTime });
1214
+ if (this.analytics && newWatchTime >= this.config.watchTimeThreshold) {
1215
+ this.analytics.trackViewDuration(
1216
+ state.currentVideo.id,
1217
+ state.currentTime,
1218
+ // Use playback position (currentTime) for API
1219
+ state.duration
1220
+ );
1221
+ }
974
1222
  }
975
1223
  }, 1e3);
976
1224
  }
@@ -984,7 +1232,7 @@ var PlayerEngine = class {
984
1232
  }
985
1233
  }
986
1234
  /**
987
- * Track completion analytics
1235
+ * Track completion analytics (when video loops/ends)
988
1236
  */
989
1237
  trackCompletion(state, loopCount) {
990
1238
  if (!this.analytics || !state.currentVideo) return;
@@ -996,6 +1244,27 @@ var PlayerEngine = class {
996
1244
  this.analytics.trackCompletion(state.currentVideo.id, state.watchTime, loopCount);
997
1245
  }
998
1246
  }
1247
+ /**
1248
+ * Track when user leaves current video (scrolls to next video)
1249
+ * Sends final analytics event before video change
1250
+ */
1251
+ trackLeaveVideo(state) {
1252
+ if (!this.analytics || !state.currentVideo) return;
1253
+ if (state.watchTime >= this.config.watchTimeThreshold) {
1254
+ this.analytics.trackViewDuration(
1255
+ state.currentVideo.id,
1256
+ state.currentTime,
1257
+ // Use currentTime (playback position) instead of watchTime
1258
+ state.duration
1259
+ );
1260
+ this.logger?.debug("[PlayerEngine] Tracked leave event", {
1261
+ videoId: state.currentVideo.id,
1262
+ watchTime: state.watchTime,
1263
+ currentTime: state.currentTime
1264
+ });
1265
+ }
1266
+ this.stopWatchTimeTracking();
1267
+ }
999
1268
  /**
1000
1269
  * Categorize media error
1001
1270
  */
@@ -1331,6 +1600,9 @@ var LifecycleManager = class {
1331
1600
  if (validSnapshot.currentVideoId !== void 0) {
1332
1601
  result.currentVideoId = validSnapshot.currentVideoId;
1333
1602
  }
1603
+ if (validSnapshot.restoreFrame !== void 0) {
1604
+ result.restoreFrame = validSnapshot.restoreFrame;
1605
+ }
1334
1606
  }
1335
1607
  this.store.setState({
1336
1608
  isRestoring: false,
@@ -1341,7 +1613,8 @@ var LifecycleManager = class {
1341
1613
  this.logger?.debug("[LifecycleManager] Session restored", {
1342
1614
  itemCount: validSnapshot.items.length,
1343
1615
  needsRevalidation,
1344
- hasPlaybackTime: this.config.restorePlaybackPosition && validSnapshot.playbackTime !== void 0
1616
+ hasPlaybackTime: this.config.restorePlaybackPosition && validSnapshot.playbackTime !== void 0,
1617
+ hasRestoreFrame: this.config.restorePlaybackPosition && validSnapshot.restoreFrame !== void 0
1345
1618
  });
1346
1619
  return result;
1347
1620
  } catch (error) {
@@ -1366,6 +1639,7 @@ var LifecycleManager = class {
1366
1639
  * @param data - Snapshot data to save
1367
1640
  * @param data.playbackTime - Current video playback position (only saved if restorePlaybackPosition config is enabled)
1368
1641
  * @param data.currentVideoId - Current video ID (only saved if restorePlaybackPosition config is enabled)
1642
+ * @param data.restoreFrame - Captured video frame at playback position (only saved if restorePlaybackPosition is enabled)
1369
1643
  */
1370
1644
  async saveSnapshot(data) {
1371
1645
  if (!this.storage) {
@@ -1394,6 +1668,9 @@ var LifecycleManager = class {
1394
1668
  if (data.currentVideoId !== void 0) {
1395
1669
  snapshot.currentVideoId = data.currentVideoId;
1396
1670
  }
1671
+ if (data.restoreFrame !== void 0) {
1672
+ snapshot.restoreFrame = data.restoreFrame;
1673
+ }
1397
1674
  }
1398
1675
  await this.storage.saveSnapshot(snapshot);
1399
1676
  const timestamp = Date.now();
@@ -1404,7 +1681,8 @@ var LifecycleManager = class {
1404
1681
  this.emitEvent({ type: "saveComplete", timestamp });
1405
1682
  this.logger?.debug("[LifecycleManager] Snapshot saved", {
1406
1683
  itemCount: data.items.length,
1407
- hasPlaybackTime: this.config.restorePlaybackPosition && data.playbackTime !== void 0
1684
+ hasPlaybackTime: this.config.restorePlaybackPosition && data.playbackTime !== void 0,
1685
+ hasRestoreFrame: this.config.restorePlaybackPosition && data.restoreFrame !== void 0
1408
1686
  });
1409
1687
  return true;
1410
1688
  } catch (error) {
@@ -2670,4 +2948,627 @@ var OptimisticManager = class {
2670
2948
  }
2671
2949
  };
2672
2950
 
2673
- export { DEFAULT_FEED_CONFIG, DEFAULT_LIFECYCLE_CONFIG, DEFAULT_OPTIMISTIC_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_PREFETCH_CONFIG, DEFAULT_RESOURCE_CONFIG, FeedManager, LifecycleManager, OptimisticManager, PlayerEngine, PlayerStatus, ResourceGovernor, calculatePrefetchIndices, calculateWindowIndices, canPause, canPlay, canSeek, computeAllocationChanges, isActiveState, isValidTransition, mapNetworkType };
2951
+ // src/comment/types.ts
2952
+ var DEFAULT_COMMENT_MANAGER_CONFIG = {
2953
+ pageSize: 20,
2954
+ repliesPageSize: 10,
2955
+ cacheTTL: 5 * 60 * 1e3,
2956
+ // 5 minutes
2957
+ maxRetries: 2,
2958
+ repliesAutoExpandThreshold: 1,
2959
+ repliesCollapseLimit: 3,
2960
+ textMaxLines: 3,
2961
+ enableOptimistic: true
2962
+ };
2963
+ var createInitialVideoCommentState = () => ({
2964
+ commentsById: /* @__PURE__ */ new Map(),
2965
+ displayOrder: [],
2966
+ totalCount: 0,
2967
+ cursor: null,
2968
+ hasMore: true,
2969
+ loading: false,
2970
+ loadingMore: false,
2971
+ error: null,
2972
+ cachedAt: 0,
2973
+ isStale: true
2974
+ });
2975
+ var createInitialCommentState = () => ({
2976
+ byVideoId: /* @__PURE__ */ new Map(),
2977
+ activeVideoId: null,
2978
+ isPosting: false,
2979
+ postError: null,
2980
+ deletingId: null
2981
+ });
2982
+
2983
+ // src/comment/CommentManager.ts
2984
+ var CommentManager = class {
2985
+ constructor(adapter, config = {}, logger) {
2986
+ this.adapter = adapter;
2987
+ /** Request deduplication: Map of key → in-flight Promise */
2988
+ this.inFlightRequests = /* @__PURE__ */ new Map();
2989
+ /** Optimistic ID counter */
2990
+ this.optimisticIdCounter = 0;
2991
+ this.config = { ...DEFAULT_COMMENT_MANAGER_CONFIG, ...config };
2992
+ this.store = createStore(createInitialCommentState);
2993
+ this.logger = logger;
2994
+ }
2995
+ // ═══════════════════════════════════════════════════════════════
2996
+ // PUBLIC API - READ OPERATIONS
2997
+ // ═══════════════════════════════════════════════════════════════
2998
+ /**
2999
+ * Load comments for a video
3000
+ *
3001
+ * Features:
3002
+ * - Cache check with TTL
3003
+ * - Request deduplication
3004
+ * - Stale data detection
3005
+ */
3006
+ async loadComments(videoId, forceRefresh = false) {
3007
+ const dedupeKey = `loadComments:${videoId}`;
3008
+ const existingRequest = this.inFlightRequests.get(dedupeKey);
3009
+ if (existingRequest) {
3010
+ this.logger?.debug(`[CommentManager] Deduped loadComments: ${videoId}`);
3011
+ return existingRequest;
3012
+ }
3013
+ const state = this.store.getState();
3014
+ const videoState = state.byVideoId.get(videoId);
3015
+ if (!forceRefresh && videoState && !this.isCacheStale(videoState)) {
3016
+ this.logger?.debug(`[CommentManager] Using cached comments: ${videoId}`);
3017
+ return;
3018
+ }
3019
+ const request = this.executeLoadComments(videoId, forceRefresh);
3020
+ this.inFlightRequests.set(dedupeKey, request);
3021
+ try {
3022
+ await request;
3023
+ } finally {
3024
+ this.inFlightRequests.delete(dedupeKey);
3025
+ }
3026
+ }
3027
+ /**
3028
+ * Load more comments (pagination)
3029
+ */
3030
+ async loadMore(videoId) {
3031
+ const state = this.store.getState();
3032
+ const videoState = state.byVideoId.get(videoId);
3033
+ if (!videoState || videoState.loading || videoState.loadingMore || !videoState.hasMore) {
3034
+ return;
3035
+ }
3036
+ const dedupeKey = `loadMore:${videoId}:${videoState.cursor}`;
3037
+ const existingRequest = this.inFlightRequests.get(dedupeKey);
3038
+ if (existingRequest) {
3039
+ return existingRequest;
3040
+ }
3041
+ const request = this.executeLoadMore(videoId);
3042
+ this.inFlightRequests.set(dedupeKey, request);
3043
+ try {
3044
+ await request;
3045
+ } finally {
3046
+ this.inFlightRequests.delete(dedupeKey);
3047
+ }
3048
+ }
3049
+ /**
3050
+ * Load replies for a comment
3051
+ */
3052
+ async loadReplies(commentId) {
3053
+ const dedupeKey = `loadReplies:${commentId}`;
3054
+ const existingRequest = this.inFlightRequests.get(dedupeKey);
3055
+ if (existingRequest) {
3056
+ return existingRequest;
3057
+ }
3058
+ const request = this.executeLoadReplies(commentId);
3059
+ this.inFlightRequests.set(dedupeKey, request);
3060
+ try {
3061
+ await request;
3062
+ } finally {
3063
+ this.inFlightRequests.delete(dedupeKey);
3064
+ }
3065
+ }
3066
+ // ═══════════════════════════════════════════════════════════════
3067
+ // PUBLIC API - WRITE OPERATIONS
3068
+ // ═══════════════════════════════════════════════════════════════
3069
+ /**
3070
+ * Post a new comment with optimistic UI
3071
+ */
3072
+ async postComment(videoId, content, currentUser) {
3073
+ if (!content.trim()) return null;
3074
+ const optimisticId = this.generateOptimisticId();
3075
+ const optimisticComment = {
3076
+ id: optimisticId,
3077
+ videoId,
3078
+ content: content.trim(),
3079
+ author: currentUser,
3080
+ likeCount: 0,
3081
+ isLiked: false,
3082
+ replyCount: 0,
3083
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3084
+ isOwner: true,
3085
+ isPending: true
3086
+ };
3087
+ if (this.config.enableOptimistic) {
3088
+ this.addOptimisticComment(videoId, optimisticComment);
3089
+ }
3090
+ this.store.setState({ isPosting: true, postError: null });
3091
+ try {
3092
+ const realComment = await this.adapter.postComment({ videoId, content: content.trim() });
3093
+ this.replaceOptimisticComment(videoId, optimisticId, realComment);
3094
+ this.logger?.debug(`[CommentManager] Posted comment: ${realComment.id}`);
3095
+ return realComment;
3096
+ } catch (error) {
3097
+ if (this.config.enableOptimistic) {
3098
+ this.removeOptimisticComment(videoId, optimisticId);
3099
+ }
3100
+ const commentError = this.createError(error);
3101
+ this.store.setState({ postError: commentError });
3102
+ this.logger?.warn("[CommentManager] Post failed", { error: commentError.message });
3103
+ return null;
3104
+ } finally {
3105
+ this.store.setState({ isPosting: false });
3106
+ }
3107
+ }
3108
+ /**
3109
+ * Post a reply with optimistic UI
3110
+ */
3111
+ async postReply(videoId, parentId, content, currentUser, replyTo) {
3112
+ if (!content.trim()) return null;
3113
+ const optimisticId = this.generateOptimisticId();
3114
+ const optimisticReply = {
3115
+ id: optimisticId,
3116
+ parentId,
3117
+ content: content.trim(),
3118
+ author: currentUser,
3119
+ likeCount: 0,
3120
+ isLiked: false,
3121
+ replyTo,
3122
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3123
+ isOwner: true,
3124
+ isPending: true
3125
+ };
3126
+ if (this.config.enableOptimistic) {
3127
+ this.addOptimisticReply(videoId, parentId, optimisticReply);
3128
+ }
3129
+ this.store.setState({ isPosting: true, postError: null });
3130
+ try {
3131
+ const realReply = await this.adapter.postReply({
3132
+ videoId,
3133
+ parentId,
3134
+ content: content.trim(),
3135
+ replyToUserId: replyTo?.id
3136
+ });
3137
+ this.replaceOptimisticReply(videoId, parentId, optimisticId, realReply);
3138
+ this.logger?.debug(`[CommentManager] Posted reply: ${realReply.id}`);
3139
+ return realReply;
3140
+ } catch (error) {
3141
+ if (this.config.enableOptimistic) {
3142
+ this.removeOptimisticReply(videoId, parentId, optimisticId);
3143
+ }
3144
+ const commentError = this.createError(error);
3145
+ this.store.setState({ postError: commentError });
3146
+ this.logger?.warn("[CommentManager] Reply failed", { error: commentError.message });
3147
+ return null;
3148
+ } finally {
3149
+ this.store.setState({ isPosting: false });
3150
+ }
3151
+ }
3152
+ /**
3153
+ * Delete a comment with optimistic UI
3154
+ */
3155
+ async deleteComment(videoId, commentId, isReply = false, parentId) {
3156
+ const backup = isReply ? this.getReply(videoId, parentId, commentId) : this.getComment(videoId, commentId);
3157
+ if (!backup) return false;
3158
+ if (this.config.enableOptimistic) {
3159
+ this.store.setState({ deletingId: commentId });
3160
+ if (isReply && parentId) {
3161
+ this.removeReplyFromStore(videoId, parentId, commentId);
3162
+ } else {
3163
+ this.removeCommentFromStore(videoId, commentId);
3164
+ }
3165
+ }
3166
+ try {
3167
+ await this.adapter.deleteComment({
3168
+ videoId,
3169
+ commentId,
3170
+ isReply,
3171
+ parentId
3172
+ });
3173
+ this.logger?.debug(`[CommentManager] Deleted ${isReply ? "reply" : "comment"}: ${commentId}`);
3174
+ return true;
3175
+ } catch (error) {
3176
+ if (this.config.enableOptimistic) {
3177
+ if (isReply && parentId) {
3178
+ this.restoreReply(videoId, parentId, backup);
3179
+ } else {
3180
+ this.restoreComment(videoId, backup);
3181
+ }
3182
+ }
3183
+ this.logger?.warn(`[CommentManager] Delete failed: ${commentId}`, {
3184
+ error: error.message
3185
+ });
3186
+ return false;
3187
+ } finally {
3188
+ this.store.setState({ deletingId: null });
3189
+ }
3190
+ }
3191
+ /**
3192
+ * Like a comment/reply with optimistic UI
3193
+ */
3194
+ async likeComment(videoId, commentId, isReply = false, parentId) {
3195
+ this.updateLikeState(videoId, commentId, true, isReply, parentId);
3196
+ try {
3197
+ await this.adapter.likeComment(commentId);
3198
+ } catch (error) {
3199
+ this.updateLikeState(videoId, commentId, false, isReply, parentId);
3200
+ this.logger?.warn(`[CommentManager] Like failed: ${commentId}`, {
3201
+ error: error.message
3202
+ });
3203
+ }
3204
+ }
3205
+ /**
3206
+ * Unlike a comment/reply with optimistic UI
3207
+ */
3208
+ async unlikeComment(videoId, commentId, isReply = false, parentId) {
3209
+ this.updateLikeState(videoId, commentId, false, isReply, parentId);
3210
+ try {
3211
+ await this.adapter.unlikeComment(commentId);
3212
+ } catch (error) {
3213
+ this.updateLikeState(videoId, commentId, true, isReply, parentId);
3214
+ this.logger?.warn(`[CommentManager] Unlike failed: ${commentId}`, {
3215
+ error: error.message
3216
+ });
3217
+ }
3218
+ }
3219
+ // ═══════════════════════════════════════════════════════════════
3220
+ // PUBLIC API - STATE ACCESSORS
3221
+ // ═══════════════════════════════════════════════════════════════
3222
+ /**
3223
+ * Get comments for a video
3224
+ */
3225
+ getComments(videoId) {
3226
+ const state = this.store.getState();
3227
+ const videoState = state.byVideoId.get(videoId);
3228
+ if (!videoState) return [];
3229
+ return videoState.displayOrder.map((id) => videoState.commentsById.get(id)).filter((c) => c !== void 0);
3230
+ }
3231
+ /**
3232
+ * Get a single comment
3233
+ */
3234
+ getComment(videoId, commentId) {
3235
+ const state = this.store.getState();
3236
+ return state.byVideoId.get(videoId)?.commentsById.get(commentId);
3237
+ }
3238
+ /**
3239
+ * Get a single reply
3240
+ */
3241
+ getReply(videoId, parentId, replyId) {
3242
+ const comment = this.getComment(videoId, parentId);
3243
+ return comment?.replies?.find((r) => r.id === replyId);
3244
+ }
3245
+ /**
3246
+ * Get video comment state
3247
+ */
3248
+ getVideoState(videoId) {
3249
+ return this.store.getState().byVideoId.get(videoId);
3250
+ }
3251
+ /**
3252
+ * Set active video ID (for comment sheet)
3253
+ */
3254
+ setActiveVideo(videoId) {
3255
+ this.store.setState({ activeVideoId: videoId });
3256
+ }
3257
+ /**
3258
+ * Get config
3259
+ */
3260
+ getConfig() {
3261
+ return this.config;
3262
+ }
3263
+ /**
3264
+ * Clear comments for a video
3265
+ */
3266
+ clearVideoComments(videoId) {
3267
+ const state = this.store.getState();
3268
+ const newByVideoId = new Map(state.byVideoId);
3269
+ newByVideoId.delete(videoId);
3270
+ this.store.setState({ byVideoId: newByVideoId });
3271
+ }
3272
+ /**
3273
+ * Clear all comments
3274
+ */
3275
+ clearAll() {
3276
+ this.store.setState(createInitialCommentState());
3277
+ }
3278
+ // ═══════════════════════════════════════════════════════════════
3279
+ // PRIVATE - EXECUTE METHODS
3280
+ // ═══════════════════════════════════════════════════════════════
3281
+ async executeLoadComments(videoId, _forceRefresh) {
3282
+ this.updateVideoState(videoId, { loading: true, error: null });
3283
+ try {
3284
+ const response = await this.adapter.getComments(videoId, null, this.config.pageSize);
3285
+ const commentsById = /* @__PURE__ */ new Map();
3286
+ const displayOrder = [];
3287
+ for (const comment of response.items) {
3288
+ commentsById.set(comment.id, comment);
3289
+ displayOrder.push(comment.id);
3290
+ }
3291
+ this.updateVideoState(videoId, {
3292
+ commentsById,
3293
+ displayOrder,
3294
+ totalCount: response.totalCount,
3295
+ cursor: response.nextCursor,
3296
+ hasMore: response.hasMore,
3297
+ loading: false,
3298
+ cachedAt: Date.now(),
3299
+ isStale: false
3300
+ });
3301
+ this.logger?.debug(
3302
+ `[CommentManager] Loaded ${response.items.length} comments for ${videoId}`
3303
+ );
3304
+ } catch (error) {
3305
+ const commentError = this.createError(error);
3306
+ this.updateVideoState(videoId, { error: commentError, loading: false });
3307
+ throw error;
3308
+ }
3309
+ }
3310
+ async executeLoadMore(videoId) {
3311
+ const videoState = this.store.getState().byVideoId.get(videoId);
3312
+ if (!videoState) return;
3313
+ this.updateVideoState(videoId, { loadingMore: true });
3314
+ try {
3315
+ const response = await this.adapter.getComments(
3316
+ videoId,
3317
+ videoState.cursor,
3318
+ this.config.pageSize
3319
+ );
3320
+ const commentsById = new Map(videoState.commentsById);
3321
+ const displayOrder = [...videoState.displayOrder];
3322
+ for (const comment of response.items) {
3323
+ if (!commentsById.has(comment.id)) {
3324
+ commentsById.set(comment.id, comment);
3325
+ displayOrder.push(comment.id);
3326
+ }
3327
+ }
3328
+ this.updateVideoState(videoId, {
3329
+ commentsById,
3330
+ displayOrder,
3331
+ cursor: response.nextCursor,
3332
+ hasMore: response.hasMore,
3333
+ loadingMore: false
3334
+ });
3335
+ this.logger?.debug(`[CommentManager] Loaded ${response.items.length} more comments`);
3336
+ } catch (error) {
3337
+ this.updateVideoState(videoId, { loadingMore: false });
3338
+ throw error;
3339
+ }
3340
+ }
3341
+ async executeLoadReplies(commentId) {
3342
+ try {
3343
+ const response = await this.adapter.getReplies(commentId, null, this.config.repliesPageSize);
3344
+ const state = this.store.getState();
3345
+ for (const [videoId, videoState] of state.byVideoId) {
3346
+ const comment = videoState.commentsById.get(commentId);
3347
+ if (comment) {
3348
+ const updatedComment = {
3349
+ ...comment,
3350
+ replies: response.items,
3351
+ repliesCursor: response.nextCursor,
3352
+ repliesLoaded: !response.hasMore
3353
+ };
3354
+ const commentsById = new Map(videoState.commentsById);
3355
+ commentsById.set(commentId, updatedComment);
3356
+ this.updateVideoState(videoId, { commentsById });
3357
+ this.logger?.debug(
3358
+ `[CommentManager] Loaded ${response.items.length} replies for ${commentId}`
3359
+ );
3360
+ break;
3361
+ }
3362
+ }
3363
+ } catch (error) {
3364
+ this.logger?.warn(`[CommentManager] Load replies failed: ${commentId}`);
3365
+ throw error;
3366
+ }
3367
+ }
3368
+ // ═══════════════════════════════════════════════════════════════
3369
+ // PRIVATE - OPTIMISTIC UPDATE HELPERS
3370
+ // ═══════════════════════════════════════════════════════════════
3371
+ addOptimisticComment(videoId, comment) {
3372
+ const state = this.store.getState();
3373
+ const videoState = state.byVideoId.get(videoId) ?? createInitialVideoCommentState();
3374
+ const commentsById = new Map(videoState.commentsById);
3375
+ commentsById.set(comment.id, comment);
3376
+ const displayOrder = [comment.id, ...videoState.displayOrder];
3377
+ this.updateVideoState(videoId, {
3378
+ commentsById,
3379
+ displayOrder,
3380
+ totalCount: videoState.totalCount + 1
3381
+ });
3382
+ }
3383
+ removeOptimisticComment(videoId, optimisticId) {
3384
+ const videoState = this.store.getState().byVideoId.get(videoId);
3385
+ if (!videoState) return;
3386
+ const commentsById = new Map(videoState.commentsById);
3387
+ commentsById.delete(optimisticId);
3388
+ const displayOrder = videoState.displayOrder.filter((id) => id !== optimisticId);
3389
+ this.updateVideoState(videoId, {
3390
+ commentsById,
3391
+ displayOrder,
3392
+ totalCount: Math.max(0, videoState.totalCount - 1)
3393
+ });
3394
+ }
3395
+ replaceOptimisticComment(videoId, optimisticId, realComment) {
3396
+ const state = this.store.getState();
3397
+ const videoState = state.byVideoId.get(videoId) ?? createInitialVideoCommentState();
3398
+ const commentsById = new Map(videoState.commentsById);
3399
+ commentsById.delete(optimisticId);
3400
+ commentsById.set(realComment.id, realComment);
3401
+ let displayOrder;
3402
+ if (videoState.displayOrder.includes(optimisticId)) {
3403
+ displayOrder = videoState.displayOrder.map(
3404
+ (id) => id === optimisticId ? realComment.id : id
3405
+ );
3406
+ } else {
3407
+ displayOrder = [realComment.id, ...videoState.displayOrder];
3408
+ }
3409
+ this.updateVideoState(videoId, { commentsById, displayOrder });
3410
+ }
3411
+ addOptimisticReply(videoId, parentId, reply) {
3412
+ const videoState = this.store.getState().byVideoId.get(videoId);
3413
+ if (!videoState) return;
3414
+ const comment = videoState.commentsById.get(parentId);
3415
+ if (!comment) return;
3416
+ const updatedComment = {
3417
+ ...comment,
3418
+ replies: [...comment.replies ?? [], reply],
3419
+ replyCount: comment.replyCount + 1
3420
+ };
3421
+ const commentsById = new Map(videoState.commentsById);
3422
+ commentsById.set(parentId, updatedComment);
3423
+ this.updateVideoState(videoId, { commentsById });
3424
+ }
3425
+ removeOptimisticReply(videoId, parentId, optimisticId) {
3426
+ const videoState = this.store.getState().byVideoId.get(videoId);
3427
+ if (!videoState) return;
3428
+ const comment = videoState.commentsById.get(parentId);
3429
+ if (!comment) return;
3430
+ const updatedComment = {
3431
+ ...comment,
3432
+ replies: comment.replies?.filter((r) => r.id !== optimisticId),
3433
+ replyCount: Math.max(0, comment.replyCount - 1)
3434
+ };
3435
+ const commentsById = new Map(videoState.commentsById);
3436
+ commentsById.set(parentId, updatedComment);
3437
+ this.updateVideoState(videoId, { commentsById });
3438
+ }
3439
+ replaceOptimisticReply(videoId, parentId, optimisticId, realReply) {
3440
+ const videoState = this.store.getState().byVideoId.get(videoId);
3441
+ if (!videoState) return;
3442
+ const comment = videoState.commentsById.get(parentId);
3443
+ if (!comment) return;
3444
+ const updatedComment = {
3445
+ ...comment,
3446
+ replies: comment.replies?.map((r) => r.id === optimisticId ? realReply : r)
3447
+ };
3448
+ const commentsById = new Map(videoState.commentsById);
3449
+ commentsById.set(parentId, updatedComment);
3450
+ this.updateVideoState(videoId, { commentsById });
3451
+ }
3452
+ removeCommentFromStore(videoId, commentId) {
3453
+ const videoState = this.store.getState().byVideoId.get(videoId);
3454
+ if (!videoState) return;
3455
+ const commentsById = new Map(videoState.commentsById);
3456
+ commentsById.delete(commentId);
3457
+ const displayOrder = videoState.displayOrder.filter((id) => id !== commentId);
3458
+ this.updateVideoState(videoId, {
3459
+ commentsById,
3460
+ displayOrder,
3461
+ totalCount: Math.max(0, videoState.totalCount - 1)
3462
+ });
3463
+ }
3464
+ removeReplyFromStore(videoId, parentId, replyId) {
3465
+ const videoState = this.store.getState().byVideoId.get(videoId);
3466
+ if (!videoState) return;
3467
+ const comment = videoState.commentsById.get(parentId);
3468
+ if (!comment) return;
3469
+ const updatedComment = {
3470
+ ...comment,
3471
+ replies: comment.replies?.filter((r) => r.id !== replyId),
3472
+ replyCount: Math.max(0, comment.replyCount - 1)
3473
+ };
3474
+ const commentsById = new Map(videoState.commentsById);
3475
+ commentsById.set(parentId, updatedComment);
3476
+ this.updateVideoState(videoId, { commentsById });
3477
+ }
3478
+ restoreComment(videoId, comment) {
3479
+ const videoState = this.store.getState().byVideoId.get(videoId);
3480
+ if (!videoState) return;
3481
+ const commentsById = new Map(videoState.commentsById);
3482
+ commentsById.set(comment.id, comment);
3483
+ const displayOrder = videoState.displayOrder.includes(comment.id) ? videoState.displayOrder : [comment.id, ...videoState.displayOrder];
3484
+ this.updateVideoState(videoId, {
3485
+ commentsById,
3486
+ displayOrder,
3487
+ totalCount: videoState.totalCount + 1
3488
+ });
3489
+ }
3490
+ restoreReply(videoId, parentId, reply) {
3491
+ const videoState = this.store.getState().byVideoId.get(videoId);
3492
+ if (!videoState) return;
3493
+ const comment = videoState.commentsById.get(parentId);
3494
+ if (!comment) return;
3495
+ const updatedComment = {
3496
+ ...comment,
3497
+ replies: [...comment.replies ?? [], reply],
3498
+ replyCount: comment.replyCount + 1
3499
+ };
3500
+ const commentsById = new Map(videoState.commentsById);
3501
+ commentsById.set(parentId, updatedComment);
3502
+ this.updateVideoState(videoId, { commentsById });
3503
+ }
3504
+ updateLikeState(videoId, commentId, isLiked, isReply, parentId) {
3505
+ const videoState = this.store.getState().byVideoId.get(videoId);
3506
+ if (!videoState) return;
3507
+ if (isReply && parentId) {
3508
+ const comment = videoState.commentsById.get(parentId);
3509
+ if (!comment) return;
3510
+ const updatedReplies = comment.replies?.map(
3511
+ (r) => r.id === commentId ? { ...r, isLiked, likeCount: r.likeCount + (isLiked ? 1 : -1) } : r
3512
+ );
3513
+ const commentsById = new Map(videoState.commentsById);
3514
+ commentsById.set(parentId, { ...comment, replies: updatedReplies });
3515
+ this.updateVideoState(videoId, { commentsById });
3516
+ } else {
3517
+ const comment = videoState.commentsById.get(commentId);
3518
+ if (!comment) return;
3519
+ const commentsById = new Map(videoState.commentsById);
3520
+ commentsById.set(commentId, {
3521
+ ...comment,
3522
+ isLiked,
3523
+ likeCount: comment.likeCount + (isLiked ? 1 : -1)
3524
+ });
3525
+ this.updateVideoState(videoId, { commentsById });
3526
+ }
3527
+ }
3528
+ // ═══════════════════════════════════════════════════════════════
3529
+ // PRIVATE - UTILITY METHODS
3530
+ // ═══════════════════════════════════════════════════════════════
3531
+ updateVideoState(videoId, partial) {
3532
+ const state = this.store.getState();
3533
+ const existing = state.byVideoId.get(videoId) ?? createInitialVideoCommentState();
3534
+ const newByVideoId = new Map(state.byVideoId);
3535
+ newByVideoId.set(videoId, { ...existing, ...partial });
3536
+ this.store.setState({ byVideoId: newByVideoId });
3537
+ }
3538
+ isCacheStale(videoState) {
3539
+ if (videoState.isStale) return true;
3540
+ if (videoState.cachedAt === 0) return true;
3541
+ return Date.now() - videoState.cachedAt > this.config.cacheTTL;
3542
+ }
3543
+ generateOptimisticId() {
3544
+ return `optimistic_${Date.now()}_${++this.optimisticIdCounter}`;
3545
+ }
3546
+ createError(error) {
3547
+ const err = error instanceof Error ? error : new Error(String(error));
3548
+ let code = "UNKNOWN";
3549
+ let recoverable = true;
3550
+ if (err.message.includes("network") || err.message.includes("fetch")) {
3551
+ code = "NETWORK_ERROR";
3552
+ } else if (err.message.includes("timeout")) {
3553
+ code = "TIMEOUT";
3554
+ } else if (err.message.includes("404") || err.message.includes("not found")) {
3555
+ code = "NOT_FOUND";
3556
+ recoverable = false;
3557
+ } else if (err.message.includes("403") || err.message.includes("forbidden")) {
3558
+ code = "FORBIDDEN";
3559
+ recoverable = false;
3560
+ } else if (err.message.includes("429") || err.message.includes("rate limit")) {
3561
+ code = "RATE_LIMITED";
3562
+ } else if (err.message.includes("500") || err.message.includes("server")) {
3563
+ code = "SERVER_ERROR";
3564
+ }
3565
+ return {
3566
+ message: err.message,
3567
+ code,
3568
+ retryCount: 0,
3569
+ recoverable
3570
+ };
3571
+ }
3572
+ };
3573
+
3574
+ export { CommentManager, DEFAULT_COMMENT_MANAGER_CONFIG, DEFAULT_FEED_CONFIG, DEFAULT_LIFECYCLE_CONFIG, DEFAULT_OPTIMISTIC_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_PREFETCH_CACHE_CONFIG, DEFAULT_PREFETCH_CONFIG, DEFAULT_RESOURCE_CONFIG, FeedManager, LifecycleManager, OptimisticManager, PlayerEngine, PlayerStatus, ResourceGovernor, calculatePrefetchIndices, calculateWindowIndices, canPause, canPlay, canSeek, computeAllocationChanges, createInitialCommentState, createInitialVideoCommentState, isActiveState, isValidTransition, mapNetworkType };