@xhub-short/core 0.1.0-beta.2 → 0.1.0-beta.20

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 +817 -629
  2. package/dist/index.js +867 -463
  3. package/package.json +4 -4
package/dist/index.d.ts CHANGED
@@ -1,14 +1,321 @@
1
- import { VideoItem, IDataSource, IStorage, PrefetchCacheData, IAnalytics, ILogger, ISessionStorage, SessionSnapshot, INetworkAdapter, IVideoLoader, IPosterLoader, VideoSource, IInteraction, CommentItem, ReplyItem, ICommentAdapter, InternalLogger, CommentAuthor } from '@xhub-short/contracts';
1
+ import { INetworkAdapter, IVideoLoader, IPosterLoader, ILogger, VideoSource, ContentItem, IDataSource, IStorage, VideoItem, PrefetchCacheData, IAnalytics, ISessionStorage, SessionSnapshot, IInteraction, CommentItem, ReplyItem, ICommentAdapter, InternalLogger, CommentAuthor, PlaylistData, PlaylistSummary, IPlaylistDataSource } from '@xhub-short/contracts';
2
2
  export { SessionSnapshot } from '@xhub-short/contracts';
3
3
  import { StoreApi } from 'zustand/vanilla';
4
4
 
5
+ /**
6
+ * Network type for prefetch strategy
7
+ * Simplified from contracts NetworkType for prefetch decisions
8
+ */
9
+ type NetworkType = 'wifi' | 'cellular' | 'offline';
10
+ /**
11
+ * Map contracts NetworkType to simplified NetworkType
12
+ */
13
+ declare function mapNetworkType(type: string): NetworkType;
14
+ /**
15
+ * Resource state for zustand store
16
+ */
17
+ interface ResourceState {
18
+ /** Currently allocated video slot indices (max 3) */
19
+ activeAllocations: Set<number>;
20
+ /** Queue of indices to preload */
21
+ preloadQueue: number[];
22
+ /** Currently focused/playing video index */
23
+ focusedIndex: number;
24
+ /** Current network type */
25
+ networkType: NetworkType;
26
+ /** Total number of items in feed (for bounds checking) */
27
+ totalItems: number;
28
+ /** Whether resource governor is active */
29
+ isActive: boolean;
30
+ /** Whether preloading is throttled due to scroll thrashing */
31
+ isThrottled: boolean;
32
+ /** Indices currently being preloaded */
33
+ preloadingIndices: Set<number>;
34
+ }
35
+ /**
36
+ * Allocation result returned by requestAllocation
37
+ */
38
+ interface AllocationResult {
39
+ /** Indices that should mount video elements */
40
+ toMount: number[];
41
+ /** Indices that should unmount video elements */
42
+ toUnmount: number[];
43
+ /** Currently active allocations after this operation */
44
+ activeAllocations: number[];
45
+ /** Whether focus change was successful */
46
+ success: boolean;
47
+ }
48
+ /**
49
+ * Prefetch configuration by network type
50
+ */
51
+ interface PrefetchConfig {
52
+ /** Number of posters to prefetch ahead (default: wifi=5, cellular=3, offline=1) */
53
+ posterCount: number;
54
+ /** Number of video segments to prefetch (default: wifi=2, cellular=1, offline=0) */
55
+ videoSegmentCount: number;
56
+ /** Whether to prefetch video at all (default: true for wifi/cellular) */
57
+ prefetchVideo: boolean;
58
+ }
59
+ /**
60
+ * Scroll thrashing configuration
61
+ * Prevents excessive preloading when user scrolls too fast
62
+ */
63
+ interface ScrollThrashingConfig {
64
+ /** Time window to measure scroll rate (ms) - default: 1000 */
65
+ windowMs: number;
66
+ /** Max focus changes allowed in window before throttling - default: 3 */
67
+ maxChangesInWindow: number;
68
+ /** Cooldown time after throttle before resuming preload (ms) - default: 500 */
69
+ cooldownMs: number;
70
+ }
71
+ /**
72
+ * ResourceGovernor configuration
73
+ */
74
+ interface ResourceConfig {
75
+ /** Maximum video DOM nodes (default: 3) */
76
+ maxAllocations?: number;
77
+ /** Focus debounce time in ms (default: 150) */
78
+ focusDebounceMs?: number;
79
+ /** Prefetch configuration overrides by network type */
80
+ prefetch?: Partial<Record<NetworkType, Partial<PrefetchConfig>>>;
81
+ /** Scroll thrashing configuration */
82
+ scrollThrashing?: Partial<ScrollThrashingConfig>;
83
+ /** Network adapter for detecting connection type */
84
+ networkAdapter?: INetworkAdapter;
85
+ /** Video loader adapter for preloading video data */
86
+ videoLoader?: IVideoLoader;
87
+ /** Poster loader adapter for preloading thumbnails */
88
+ posterLoader?: IPosterLoader;
89
+ /** Logger adapter */
90
+ logger?: ILogger;
91
+ }
92
+ /**
93
+ * Default prefetch configuration by network type
94
+ */
95
+ declare const DEFAULT_PREFETCH_CONFIG: Record<NetworkType, PrefetchConfig>;
96
+ /**
97
+ * Default resource configuration
98
+ */
99
+ declare const DEFAULT_RESOURCE_CONFIG: Required<Omit<ResourceConfig, 'networkAdapter' | 'videoLoader' | 'posterLoader' | 'logger' | 'prefetch' | 'scrollThrashing'>> & {
100
+ prefetch: Record<NetworkType, PrefetchConfig>;
101
+ scrollThrashing: ScrollThrashingConfig;
102
+ };
103
+ /**
104
+ * Resource events for external listening
105
+ */
106
+ type ResourceEvent = {
107
+ type: 'allocationChange';
108
+ toMount: number[];
109
+ toUnmount: number[];
110
+ } | {
111
+ type: 'focusChange';
112
+ index: number;
113
+ previousIndex: number;
114
+ } | {
115
+ type: 'networkChange';
116
+ networkType: NetworkType;
117
+ } | {
118
+ type: 'prefetchRequest';
119
+ indices: number[];
120
+ };
121
+ /**
122
+ * Resource event listener
123
+ */
124
+ type ResourceEventListener = (event: ResourceEvent) => void;
125
+
126
+ /**
127
+ * Video source getter function type
128
+ * ResourceGovernor needs this to get video sources for preloading
129
+ */
130
+ type VideoSourceGetter = (index: number) => {
131
+ id: string;
132
+ source: VideoSource;
133
+ poster?: string;
134
+ } | null;
135
+ /**
136
+ * ResourceGovernor - Manages video DOM allocation and prefetch strategy
137
+ *
138
+ * Features:
139
+ * - Sliding window allocation (max 3 video DOM nodes)
140
+ * - Focus debouncing for smooth swipe
141
+ * - Network-aware prefetch strategy
142
+ * - Event system for UI synchronization
143
+ *
144
+ * Memory Strategy:
145
+ * - Only 3 video elements exist in DOM at any time
146
+ * - Sliding window: [previous, current, next]
147
+ * - When user scrolls, we unmount far elements and mount new ones
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const governor = new ResourceGovernor({ maxAllocations: 3 });
152
+ *
153
+ * // Initialize with feed size
154
+ * governor.setTotalItems(20);
155
+ * governor.activate();
156
+ *
157
+ * // Handle scroll/swipe
158
+ * governor.setFocusedIndex(5); // Debounced
159
+ *
160
+ * // Listen to allocation changes
161
+ * governor.addEventListener((event) => {
162
+ * if (event.type === 'allocationChange') {
163
+ * event.toMount.forEach(i => mountVideoAt(i));
164
+ * event.toUnmount.forEach(i => unmountVideoAt(i));
165
+ * }
166
+ * });
167
+ * ```
168
+ */
169
+ declare class ResourceGovernor {
170
+ /** Zustand vanilla store - Single Source of Truth */
171
+ readonly store: StoreApi<ResourceState>;
172
+ /** Resolved configuration */
173
+ private readonly config;
174
+ /** Network adapter */
175
+ private readonly networkAdapter?;
176
+ /** Video loader adapter for preloading video data */
177
+ private readonly videoLoader?;
178
+ /** Poster loader adapter for preloading thumbnails */
179
+ private readonly posterLoader?;
180
+ /** Logger adapter */
181
+ private readonly logger?;
182
+ /** Event listeners */
183
+ private readonly eventListeners;
184
+ /** Focus debounce timer */
185
+ private focusDebounceTimer;
186
+ /** Pending focused index (before debounce completes) */
187
+ private pendingFocusedIndex;
188
+ /** Network change unsubscribe function */
189
+ private networkUnsubscribe?;
190
+ /** Video source getter (injected via setVideoSourceGetter) */
191
+ private videoSourceGetter?;
192
+ /** Scroll thrashing detection - timestamps of recent focus changes */
193
+ private focusChangeTimestamps;
194
+ /** Scroll thrashing cooldown timer */
195
+ private thrashingCooldownTimer;
196
+ constructor(config?: ResourceConfig);
197
+ /**
198
+ * Activate the resource governor
199
+ * Starts network monitoring and performs initial allocation
200
+ */
201
+ activate(): Promise<void>;
202
+ /**
203
+ * Deactivate the resource governor
204
+ * Stops network monitoring and clears allocations
205
+ */
206
+ deactivate(): void;
207
+ /**
208
+ * Destroy the resource governor
209
+ */
210
+ destroy(): void;
211
+ /**
212
+ * Set total number of items in feed
213
+ */
214
+ setTotalItems(count: number): void;
215
+ /**
216
+ * Set focused index with debouncing
217
+ * This is called during scroll/swipe
218
+ */
219
+ setFocusedIndex(index: number): void;
220
+ /**
221
+ * Set focused index immediately (skip debounce)
222
+ * Use this for programmatic navigation, not scroll
223
+ */
224
+ setFocusedIndexImmediate(index: number): void;
225
+ /**
226
+ * Request allocation for given indices
227
+ * Returns what needs to be mounted/unmounted
228
+ */
229
+ requestAllocation(indices: number[]): AllocationResult;
230
+ /**
231
+ * Get current active allocations
232
+ */
233
+ getActiveAllocations(): number[];
234
+ /**
235
+ * Check if an index is currently allocated
236
+ */
237
+ isAllocated(index: number): boolean;
238
+ /**
239
+ * Get current network type
240
+ */
241
+ getNetworkType(): NetworkType;
242
+ /**
243
+ * Get prefetch configuration for current network
244
+ */
245
+ getPrefetchConfig(): PrefetchConfig;
246
+ /**
247
+ * Set video source getter function
248
+ * This is called by SDK to provide video data for preloading
249
+ *
250
+ * @param getter - Function that returns video info for a given index
251
+ */
252
+ setVideoSourceGetter(getter: VideoSourceGetter): void;
253
+ /**
254
+ * Check if preloading is currently throttled due to scroll thrashing
255
+ */
256
+ isPreloadThrottled(): boolean;
257
+ /**
258
+ * Get indices currently being preloaded
259
+ */
260
+ getPreloadingIndices(): number[];
261
+ /**
262
+ * Manually trigger preload for specific indices
263
+ * Respects throttling and network conditions
264
+ */
265
+ triggerPreload(indices: number[]): Promise<void>;
266
+ /**
267
+ * Add event listener
268
+ */
269
+ addEventListener(listener: ResourceEventListener): () => void;
270
+ /**
271
+ * Remove event listener
272
+ */
273
+ removeEventListener(listener: ResourceEventListener): void;
274
+ /**
275
+ * Initialize network detection
276
+ */
277
+ private initializeNetwork;
278
+ /**
279
+ * Perform allocation for a given focused index
280
+ */
281
+ private performAllocation;
282
+ /**
283
+ * Update prefetch queue based on current state
284
+ */
285
+ private updatePrefetchQueue;
286
+ /**
287
+ * Track focus change timestamp for scroll thrashing detection
288
+ */
289
+ private trackFocusChange;
290
+ /**
291
+ * Cancel all in-progress preloads
292
+ */
293
+ private cancelAllPreloads;
294
+ /**
295
+ * Execute video preloading for given indices
296
+ */
297
+ private executePreload;
298
+ /**
299
+ * Internal helper to preload a single video
300
+ */
301
+ private preloadOne;
302
+ /**
303
+ * Execute poster preloading for given indices
304
+ */
305
+ private executePreloadPosters;
306
+ /**
307
+ * Emit event to all listeners
308
+ */
309
+ private emitEvent;
310
+ }
311
+
5
312
  /**
6
313
  * Feed state for zustand store
7
314
  */
8
315
  interface FeedState {
9
- /** Normalized video data - Map for O(1) lookup */
10
- itemsById: Map<string, VideoItem>;
11
- /** Ordered list of video IDs for rendering */
316
+ /** Normalized content data - Map for O(1) lookup */
317
+ itemsById: Map<string, ContentItem>;
318
+ /** Ordered list of content IDs for rendering */
12
319
  displayOrder: string[];
13
320
  /** Initial loading state */
14
321
  loading: boolean;
@@ -53,8 +360,8 @@ interface FeedConfig {
53
360
  /** Whether to enable SWR pattern (default: true) */
54
361
  enableSWR?: boolean;
55
362
  /**
56
- * Maximum number of videos to keep in memory cache
57
- * When exceeded, older videos will be evicted (LRU policy)
363
+ * Maximum number of items to keep in memory cache
364
+ * When exceeded, older items will be evicted (LRU policy)
58
365
  * @default 100
59
366
  */
60
367
  maxCacheSize?: number;
@@ -92,11 +399,14 @@ interface PrefetchCacheConfig {
92
399
  */
93
400
  enabled: boolean;
94
401
  /**
95
- * Maximum number of videos to cache
402
+ * Maximum number of items to cache
96
403
  * Higher = more instant content, but more storage
97
- * @default 10
98
404
  */
99
- maxVideos: number;
405
+ maxItems: number;
406
+ /**
407
+ * @deprecated Use maxItems instead. Will be removed in v3.0
408
+ */
409
+ maxVideos?: number;
100
410
  /**
101
411
  * Storage key for prefetch cache
102
412
  * @default 'sv-prefetch-cache'
@@ -104,8 +414,8 @@ interface PrefetchCacheConfig {
104
414
  storageKey: string;
105
415
  /**
106
416
  * Enable dynamic cache eviction
107
- * When user scrolls past cached videos, remove them from cache
108
- * Prevents user from rewatching same videos on reload
417
+ * When user scrolls past cached items, remove them from cache
418
+ * Prevents user from rewatching same items on reload
109
419
  * @default true
110
420
  */
111
421
  enableDynamicEviction: boolean;
@@ -122,7 +432,7 @@ interface PrefetchCacheConfig {
122
432
  declare const DEFAULT_PREFETCH_CACHE_CONFIG: PrefetchCacheConfig;
123
433
 
124
434
  /**
125
- * FeedManager - Manages video feed data with zustand/vanilla store
435
+ * FeedManager - Manages content feed data with zustand/vanilla store
126
436
  *
127
437
  * Features:
128
438
  * - Data normalization (Map for O(1) lookup + ordered IDs)
@@ -148,7 +458,8 @@ declare const DEFAULT_PREFETCH_CACHE_CONFIG: PrefetchCacheConfig;
148
458
  * ```
149
459
  */
150
460
  declare class FeedManager {
151
- private readonly dataSource;
461
+ private dataSource;
462
+ private readonly logger?;
152
463
  /** Zustand vanilla store - Single Source of Truth */
153
464
  readonly store: StoreApi<FeedState>;
154
465
  /** Resolved configuration */
@@ -169,7 +480,52 @@ declare class FeedManager {
169
480
  * Used for garbage collection
170
481
  */
171
482
  private accessOrder;
172
- constructor(dataSource: IDataSource, config?: FeedConfig, storage?: IStorage, prefetchConfig?: Partial<PrefetchCacheConfig>);
483
+ /**
484
+ * Track items that have already triggered predictive preload
485
+ * to avoid duplicate requests.
486
+ */
487
+ private preloadedItemIds;
488
+ /**
489
+ * Recommend feed snapshot — preserved when switching to playlist mode.
490
+ * Stored here (not in React refs) because FeedManager is a singleton
491
+ * that survives component remounts caused by conditional rendering.
492
+ */
493
+ private recommendSnapshot;
494
+ constructor(dataSource: IDataSource, config?: FeedConfig, storage?: IStorage, prefetchConfig?: Partial<PrefetchCacheConfig>, logger?: ILogger | undefined);
495
+ /** Static memory cache for explicit prefetching */
496
+ private static globalMemoryCache;
497
+ /**
498
+ * Prefetch feed data and store in global memory cache
499
+ *
500
+ * @param dataSource - Data source to fetch from
501
+ * @param options - Prefetch options
502
+ */
503
+ static prefetch(dataSource: IDataSource, options?: {
504
+ ttl?: number;
505
+ }): Promise<void>;
506
+ /**
507
+ * Check if prefetch cache exists and is valid
508
+ */
509
+ static hasPrefetchCache(): boolean;
510
+ /**
511
+ * Clear prefetch cache
512
+ */
513
+ static clearPrefetchCache(): void;
514
+ /**
515
+ * Get current data source
516
+ */
517
+ getDataSource(): IDataSource;
518
+ /**
519
+ * Update data source dynamically
520
+ * Used for switching between Recommendation and Playlist modes
521
+ *
522
+ * @param dataSource - New data source adapter
523
+ * @param options - Options for state transition
524
+ */
525
+ setDataSource(dataSource: IDataSource, options?: {
526
+ /** Whether to reset the feed state immediately */
527
+ reset?: boolean;
528
+ }): void;
173
529
  /**
174
530
  * Load initial feed data
175
531
  *
@@ -182,7 +538,9 @@ declare class FeedManager {
182
538
  * - If a request for the same cursor is already in-flight, returns the existing Promise
183
539
  * - Prevents duplicate API calls from rapid UI interactions
184
540
  */
185
- loadInitial(): Promise<void>;
541
+ loadInitial(options?: {
542
+ replace?: boolean;
543
+ }): Promise<void>;
186
544
  /**
187
545
  * Internal: Execute load initial logic
188
546
  */
@@ -207,18 +565,59 @@ declare class FeedManager {
207
565
  */
208
566
  revalidate(): Promise<void>;
209
567
  /**
210
- * Get a video by ID
211
- * Also updates LRU access time for garbage collection
568
+ * Handle playback progress and trigger predictive preloading
569
+ *
570
+ * @param itemId - ID of the currently playing item
571
+ * @param progress - Current playback progress (0-1)
572
+ * @param governor - Resource governor to trigger preload
573
+ * @param threshold - Progress threshold to trigger preload (default: 0.2)
574
+ */
575
+ handlePlaybackProgress(itemId: string, progress: number, governor: ResourceGovernor, threshold?: number): void;
576
+ getItem(id: string): ContentItem | undefined;
577
+ /**
578
+ * Get ordered list of items
579
+ */
580
+ getItems(): ContentItem[];
581
+ /**
582
+ * Update an item in the feed (for optimistic updates)
583
+ */
584
+ updateItem(id: string, updates: Partial<ContentItem>): void;
585
+ /**
586
+ * Replace all items in the feed (e.g. for Playlist synchronization)
587
+ */
588
+ replaceItems(items: ContentItem[]): void;
589
+ /**
590
+ * @deprecated Use getItem instead
212
591
  */
213
592
  getVideo(id: string): VideoItem | undefined;
214
593
  /**
215
- * Get ordered list of videos
594
+ * @deprecated Use getItems instead
216
595
  */
217
596
  getVideos(): VideoItem[];
218
597
  /**
219
- * Update a video in the feed (for optimistic updates)
598
+ * @deprecated Use updateItem instead
220
599
  */
221
600
  updateVideo(id: string, updates: Partial<VideoItem>): void;
601
+ /**
602
+ * Remove an item from the feed
603
+ *
604
+ * Used for:
605
+ * - Report: Remove reported content from feed
606
+ * - Not Interested: Remove content user doesn't want to see
607
+ *
608
+ * @param id - Content ID to remove
609
+ * @returns true if item was removed, false if not found
610
+ *
611
+ * @example
612
+ * ```typescript
613
+ * // User reports a content item
614
+ * const wasRemoved = feedManager.removeItem(itemId);
615
+ * if (wasRemoved) {
616
+ * // Navigate to next item
617
+ * }
618
+ * ```
619
+ */
620
+ removeItem(id: string): boolean;
222
621
  /**
223
622
  * Check if data is stale and needs revalidation
224
623
  */
@@ -241,20 +640,37 @@ declare class FeedManager {
241
640
  * Used by LifecycleManager to restore state without API call.
242
641
  * This bypasses normal data flow for state restoration.
243
642
  *
244
- * @param items - Video items from snapshot
643
+ * @param items - Content items from snapshot
245
644
  * @param cursor - Pagination cursor from snapshot
246
645
  * @param options - Additional hydration options
247
646
  */
248
- hydrateFromSnapshot(items: VideoItem[], cursor: string | null, options?: {
249
- /** Whether to mark data as stale for background revalidation */
250
- markAsStale?: boolean;
251
- }): void;
647
+ hydrateFromSnapshot(items: ContentItem[], cursor: string | null, options?: {
648
+ /** Whether to mark data as stale for background revalidation */
649
+ markAsStale?: boolean;
650
+ }): void;
651
+ /**
652
+ * Save the current recommend feed state before switching to playlist mode.
653
+ *
654
+ * @param activeIndex - Current scroll position (active item index)
655
+ */
656
+ saveRecommendSnapshot(activeIndex: number): void;
657
+ /**
658
+ * Restore the recommend feed state after exiting playlist mode.
659
+ * Delegates to hydrateFromSnapshot() internally to avoid duplicating hydration logic.
660
+ *
661
+ * @returns The saved activeIndex, or null if no snapshot was available
662
+ */
663
+ restoreRecommendSnapshot(): number | null;
664
+ /**
665
+ * Check if a recommend snapshot exists (for conditional logic in hooks)
666
+ */
667
+ hasRecommendSnapshot(): boolean;
252
668
  /**
253
669
  * Update prefetch cache with current feed tail
254
670
  * Called automatically after loadInitial() and loadMore()
255
671
  *
256
- * Strategy: Cache the LAST N videos (tail of feed)
257
- * These are videos user hasn't seen yet, perfect for instant display
672
+ * Strategy: Cache the LAST N items (tail of feed)
673
+ * These are items user hasn't seen yet, perfect for instant display
258
674
  */
259
675
  updatePrefetchCache(): void;
260
676
  /**
@@ -271,21 +687,37 @@ declare class FeedManager {
271
687
  * Marks data as stale to trigger background revalidation
272
688
  */
273
689
  hydrateFromPrefetchCache(cache: PrefetchCacheData): void;
690
+ /**
691
+ * Load prefetch cache synchronously (zero-flash optimization)
692
+ *
693
+ * Uses `storage.getSync()` for synchronous localStorage access.
694
+ * This allows hydrating the feed during React's synchronous render phase,
695
+ * preventing any flash of loading/empty state when cached data exists.
696
+ *
697
+ * Returns null if:
698
+ * - Prefetch cache is disabled
699
+ * - No storage adapter
700
+ * - Storage doesn't support sync reads (e.g., IndexedDB)
701
+ * - No cached data
702
+ *
703
+ * @returns Cached feed data or null
704
+ */
705
+ loadPrefetchCacheSync(): PrefetchCacheData | null;
274
706
  /**
275
707
  * Clear prefetch cache
276
708
  * Call when user logs out or data should be invalidated
277
709
  */
278
710
  clearPrefetchCache(): Promise<void>;
279
711
  /**
280
- * Evict videos that user has scrolled past from prefetch cache
712
+ * Evict items that user has scrolled past from prefetch cache
281
713
  * Called when user's focusedIndex changes
282
714
  *
283
- * Strategy: Remove all videos at or before current position
284
- * This ensures user doesn't rewatch videos on reload
715
+ * Strategy: Remove all items at or before current position
716
+ * This ensures user doesn't rewatch items on reload
285
717
  *
286
- * @param currentIndex - Current focused video index in feed
718
+ * @param currentIndex - Current focused item index in feed
287
719
  */
288
- evictViewedVideosFromCache(currentIndex: number): Promise<void>;
720
+ evictViewedItemsFromCache(currentIndex: number): Promise<void>;
289
721
  /**
290
722
  * Get prefetch cache configuration (for external access)
291
723
  */
@@ -295,15 +727,15 @@ declare class FeedManager {
295
727
  */
296
728
  private fetchWithRetry;
297
729
  /**
298
- * Add videos with deduplication
730
+ * Add items with deduplication
299
731
  * Triggers garbage collection if cache exceeds maxCacheSize
300
732
  */
301
- private addVideos;
733
+ private addItems;
302
734
  /**
303
- * Merge videos (for SWR revalidation)
304
- * Updates existing videos, adds new ones at the beginning
735
+ * Merge items (for SWR revalidation)
736
+ * Updates existing items, adds new ones at the beginning
305
737
  */
306
- private mergeVideos;
738
+ private mergeItems;
307
739
  /**
308
740
  * Handle and categorize errors
309
741
  */
@@ -320,9 +752,9 @@ declare class FeedManager {
320
752
  * Run garbage collection using LRU (Least Recently Used) policy
321
753
  *
322
754
  * When cache size exceeds maxCacheSize:
323
- * 1. Sort videos by last access time (oldest first)
324
- * 2. Evict oldest videos until cache is within limit
325
- * 3. Keep videos that are currently in viewport (most recent in displayOrder)
755
+ * 1. Sort items by last access time (oldest first)
756
+ * 2. Evict oldest items until cache is within limit
757
+ * 3. Keep items that are currently in viewport (most recent in displayOrder)
326
758
  *
327
759
  * @returns Number of evicted items
328
760
  */
@@ -769,6 +1201,7 @@ declare class PlayerEngine {
769
1201
  private emitEvent;
770
1202
  /**
771
1203
  * Start watch time tracking
1204
+ * Increments watchTime every second and sends analytics heartbeat
772
1205
  */
773
1206
  private startWatchTimeTracking;
774
1207
  /**
@@ -776,9 +1209,14 @@ declare class PlayerEngine {
776
1209
  */
777
1210
  private stopWatchTimeTracking;
778
1211
  /**
779
- * Track completion analytics
1212
+ * Track completion analytics (when video loops/ends)
780
1213
  */
781
1214
  private trackCompletion;
1215
+ /**
1216
+ * Track when user leaves current video (scrolls to next video)
1217
+ * Sends final analytics event before video change
1218
+ */
1219
+ private trackLeaveVideo;
782
1220
  /**
783
1221
  * Categorize media error
784
1222
  */
@@ -838,543 +1276,244 @@ declare function isValidTransition(from: PlayerStatus, to: PlayerStatus): boolea
838
1276
  /**
839
1277
  * Check if player is in a "active" state (can receive playback commands)
840
1278
  */
841
- declare function isActiveState(status: PlayerStatus): boolean;
842
- /**
843
- * Check if player can start playback
844
- */
845
- declare function canPlay(status: PlayerStatus): boolean;
846
- /**
847
- * Check if player can pause
848
- */
849
- declare function canPause(status: PlayerStatus): boolean;
850
- /**
851
- * Check if player can seek
852
- */
853
- declare function canSeek(status: PlayerStatus): boolean;
854
-
855
- /**
856
- * Lifecycle state for zustand store
857
- */
858
- interface LifecycleState {
859
- /** Whether manager is initialized */
860
- isInitialized: boolean;
861
- /** Whether there's a pending save operation */
862
- isSaving: boolean;
863
- /** Whether there's a pending restore operation */
864
- isRestoring: boolean;
865
- /** Last saved timestamp */
866
- lastSavedAt: number | null;
867
- /** Last restored timestamp */
868
- lastRestoredAt: number | null;
869
- /** Whether the restored snapshot needs revalidation */
870
- needsRevalidation: boolean;
871
- /** Current visibility state */
872
- visibilityState: DocumentVisibilityState;
873
- }
874
- /**
875
- * Restore result
876
- */
877
- interface RestoreResult {
878
- /** Whether restore was successful */
879
- success: boolean;
880
- /** Restored snapshot data (null if no valid snapshot) */
881
- snapshot: SessionSnapshot | null;
882
- /** Whether the restored data is stale and needs revalidation */
883
- needsRevalidation: boolean;
884
- /** Reason for failure (if any) */
885
- reason?: 'no_snapshot' | 'expired' | 'invalid' | 'error';
886
- /** Playback time in seconds (only present if restorePlaybackPosition config is enabled) */
887
- playbackTime?: number;
888
- /** Video ID that was playing (only present if restorePlaybackPosition config is enabled) */
889
- currentVideoId?: string;
890
- /** Captured video frame at playback position (base64 JPEG, only present if restorePlaybackPosition is enabled) */
891
- restoreFrame?: string;
892
- }
893
- /**
894
- * LifecycleManager configuration
895
- */
896
- interface LifecycleConfig {
897
- /** Storage adapter for persistence */
898
- storage?: ISessionStorage;
899
- /** Logger adapter */
900
- logger?: ILogger;
901
- /** Snapshot expiry time in ms (default: 24 hours) */
902
- snapshotExpiryMs?: number;
903
- /** Revalidation threshold in ms (default: 5 minutes) */
904
- revalidationThresholdMs?: number;
905
- /** Auto-save on visibility change (default: true) */
906
- autoSaveOnHidden?: boolean;
907
- /**
908
- * Enable restoring video playback position (default: false)
909
- * When enabled, saves and restores the exact playback time (seconds)
910
- * so video can seek() to the exact position user was watching
911
- */
912
- restorePlaybackPosition?: boolean;
913
- /** SDK version for compatibility */
914
- version?: string;
915
- }
916
- /**
917
- * Default lifecycle configuration
918
- */
919
- declare const DEFAULT_LIFECYCLE_CONFIG: Required<Omit<LifecycleConfig, 'storage' | 'logger'>>;
920
- /**
921
- * Lifecycle events for external listening
922
- */
923
- type LifecycleEvent = {
924
- type: 'saveStart';
925
- } | {
926
- type: 'saveComplete';
927
- timestamp: number;
928
- } | {
929
- type: 'saveFailed';
930
- error: Error;
931
- } | {
932
- type: 'restoreStart';
933
- } | {
934
- type: 'restoreComplete';
935
- result: RestoreResult;
936
- } | {
937
- type: 'visibilityChange';
938
- state: DocumentVisibilityState;
939
- };
940
- /**
941
- * Lifecycle event listener
942
- */
943
- type LifecycleEventListener = (event: LifecycleEvent) => void;
944
-
945
- /**
946
- * LifecycleManager - Handles session persistence and restoration
947
- *
948
- * Strategy: "State Persistence, DOM Destruction"
949
- * - Save snapshot to storage when user leaves
950
- * - Restore state from storage when user returns
951
- * - Video DOM nodes are destroyed to free RAM
952
- *
953
- * Features:
954
- * - Auto-save on visibility change (optional)
955
- * - Snapshot expiry (24 hours default)
956
- * - Stale data detection for revalidation
957
- * - Event system for UI coordination
958
- *
959
- * @example
960
- * ```typescript
961
- * const lifecycle = new LifecycleManager({ storage, logger });
962
- *
963
- * // Initialize and attempt restore
964
- * const result = await lifecycle.initialize();
965
- * if (result.success) {
966
- * feedManager.restoreFromSnapshot(result.snapshot);
967
- * if (result.needsRevalidation) {
968
- * feedManager.revalidate();
969
- * }
970
- * }
971
- *
972
- * // Save snapshot before leaving
973
- * lifecycle.saveSnapshot({
974
- * items: feedManager.getVideos(),
975
- * cursor: feedManager.store.getState().cursor,
976
- * focusedIndex: resourceGovernor.store.getState().focusedIndex,
977
- * });
978
- * ```
979
- */
980
- declare class LifecycleManager {
981
- /** Zustand vanilla store - Single Source of Truth */
982
- readonly store: StoreApi<LifecycleState>;
983
- /** Resolved configuration */
984
- private readonly config;
985
- /** Storage adapter */
986
- private readonly storage?;
987
- /** Logger adapter */
988
- private readonly logger?;
989
- /** Event listeners */
990
- private readonly eventListeners;
991
- /** Visibility change handler reference (for cleanup) */
992
- private visibilityHandler?;
993
- /** Pending save data (for debouncing) */
994
- private pendingSaveData?;
995
- constructor(config?: LifecycleConfig);
996
- /**
997
- * Initialize lifecycle manager and attempt to restore session
998
- */
999
- initialize(): Promise<RestoreResult>;
1000
- /**
1001
- * Destroy lifecycle manager and cleanup
1002
- */
1003
- destroy(): void;
1004
- /**
1005
- * Restore session from storage
1006
- */
1007
- restoreSession(): Promise<RestoreResult>;
1008
- /**
1009
- * Save session snapshot to storage
1010
- *
1011
- * @param data - Snapshot data to save
1012
- * @param data.playbackTime - Current video playback position (only saved if restorePlaybackPosition config is enabled)
1013
- * @param data.currentVideoId - Current video ID (only saved if restorePlaybackPosition config is enabled)
1014
- * @param data.restoreFrame - Captured video frame at playback position (only saved if restorePlaybackPosition is enabled)
1015
- */
1016
- saveSnapshot(data: {
1017
- items: VideoItem[];
1018
- cursor: string | null;
1019
- focusedIndex: number;
1020
- scrollPosition?: number;
1021
- /** Current video playback time in seconds (only used when restorePlaybackPosition is enabled) */
1022
- playbackTime?: number;
1023
- /** Current video ID (only used when restorePlaybackPosition is enabled) */
1024
- currentVideoId?: string;
1025
- /** Captured video frame at playback position (only used when restorePlaybackPosition is enabled) */
1026
- restoreFrame?: string;
1027
- }): Promise<boolean>;
1028
- /**
1029
- * Clear saved snapshot
1030
- */
1031
- clearSnapshot(): Promise<void>;
1032
- /**
1033
- * Mark that pending data should be saved (for use with debouncing)
1034
- *
1035
- * @param data - Data to save when flush is called
1036
- */
1037
- setPendingSave(data: Omit<SessionSnapshot, 'savedAt' | 'version'>): void;
1038
- /**
1039
- * Check if restorePlaybackPosition config is enabled
1040
- * SDK can use this to decide whether to collect playbackTime
1041
- */
1042
- isPlaybackPositionRestoreEnabled(): boolean;
1043
- /**
1044
- * Flush pending save (called on visibility hidden)
1045
- */
1046
- flushPendingSave(): Promise<void>;
1047
- /**
1048
- * Handle visibility state change
1049
- */
1050
- onVisibilityChange(state: DocumentVisibilityState): void;
1051
- /**
1052
- * Get current visibility state
1053
- */
1054
- getVisibilityState(): DocumentVisibilityState;
1055
- /**
1056
- * Add event listener
1057
- */
1058
- addEventListener(listener: LifecycleEventListener): () => void;
1059
- /**
1060
- * Remove event listener
1061
- */
1062
- removeEventListener(listener: LifecycleEventListener): void;
1063
- /**
1064
- * Setup visibility change listener
1065
- */
1066
- private setupVisibilityListener;
1067
- /**
1068
- * Validate snapshot data
1069
- */
1070
- private validateSnapshot;
1071
- /**
1072
- * Check if snapshot is stale (needs revalidation)
1073
- */
1074
- private isSnapshotStale;
1075
- /**
1076
- * Emit event to all listeners
1077
- */
1078
- private emitEvent;
1079
- }
1080
-
1081
- /**
1082
- * Network type for prefetch strategy
1083
- * Simplified from contracts NetworkType for prefetch decisions
1084
- */
1085
- type NetworkType = 'wifi' | 'cellular' | 'offline';
1279
+ declare function isActiveState(status: PlayerStatus): boolean;
1086
1280
  /**
1087
- * Map contracts NetworkType to simplified NetworkType
1281
+ * Check if player can start playback
1088
1282
  */
1089
- declare function mapNetworkType(type: string): NetworkType;
1283
+ declare function canPlay(status: PlayerStatus): boolean;
1090
1284
  /**
1091
- * Resource state for zustand store
1285
+ * Check if player can pause
1092
1286
  */
1093
- interface ResourceState {
1094
- /** Currently allocated video slot indices (max 3) */
1095
- activeAllocations: Set<number>;
1096
- /** Queue of indices to preload */
1097
- preloadQueue: number[];
1098
- /** Currently focused/playing video index */
1099
- focusedIndex: number;
1100
- /** Current network type */
1101
- networkType: NetworkType;
1102
- /** Total number of items in feed (for bounds checking) */
1103
- totalItems: number;
1104
- /** Whether resource governor is active */
1105
- isActive: boolean;
1106
- /** Whether preloading is throttled due to scroll thrashing */
1107
- isThrottled: boolean;
1108
- /** Indices currently being preloaded */
1109
- preloadingIndices: Set<number>;
1110
- }
1287
+ declare function canPause(status: PlayerStatus): boolean;
1111
1288
  /**
1112
- * Allocation result returned by requestAllocation
1289
+ * Check if player can seek
1113
1290
  */
1114
- interface AllocationResult {
1115
- /** Indices that should mount video elements */
1116
- toMount: number[];
1117
- /** Indices that should unmount video elements */
1118
- toUnmount: number[];
1119
- /** Currently active allocations after this operation */
1120
- activeAllocations: number[];
1121
- /** Whether focus change was successful */
1122
- success: boolean;
1123
- }
1291
+ declare function canSeek(status: PlayerStatus): boolean;
1292
+
1124
1293
  /**
1125
- * Prefetch configuration by network type
1294
+ * Lifecycle state for zustand store
1126
1295
  */
1127
- interface PrefetchConfig {
1128
- /** Number of posters to prefetch ahead (default: wifi=5, cellular=3, offline=1) */
1129
- posterCount: number;
1130
- /** Number of video segments to prefetch (default: wifi=2, cellular=1, offline=0) */
1131
- videoSegmentCount: number;
1132
- /** Whether to prefetch video at all (default: true for wifi/cellular) */
1133
- prefetchVideo: boolean;
1296
+ interface LifecycleState {
1297
+ /** Whether manager is initialized */
1298
+ isInitialized: boolean;
1299
+ /** Whether there's a pending save operation */
1300
+ isSaving: boolean;
1301
+ /** Whether there's a pending restore operation */
1302
+ isRestoring: boolean;
1303
+ /** Last saved timestamp */
1304
+ lastSavedAt: number | null;
1305
+ /** Last restored timestamp */
1306
+ lastRestoredAt: number | null;
1307
+ /** Whether the restored snapshot needs revalidation */
1308
+ needsRevalidation: boolean;
1309
+ /** Current visibility state */
1310
+ visibilityState: DocumentVisibilityState;
1134
1311
  }
1135
1312
  /**
1136
- * Scroll thrashing configuration
1137
- * Prevents excessive preloading when user scrolls too fast
1313
+ * Restore result
1138
1314
  */
1139
- interface ScrollThrashingConfig {
1140
- /** Time window to measure scroll rate (ms) - default: 1000 */
1141
- windowMs: number;
1142
- /** Max focus changes allowed in window before throttling - default: 3 */
1143
- maxChangesInWindow: number;
1144
- /** Cooldown time after throttle before resuming preload (ms) - default: 500 */
1145
- cooldownMs: number;
1315
+ interface RestoreResult {
1316
+ /** Whether restore was successful */
1317
+ success: boolean;
1318
+ /** Restored snapshot data (null if no valid snapshot) */
1319
+ snapshot: SessionSnapshot | null;
1320
+ /** Whether the restored data is stale and needs revalidation */
1321
+ needsRevalidation: boolean;
1322
+ /** Reason for failure (if any) */
1323
+ reason?: 'no_snapshot' | 'expired' | 'invalid' | 'error';
1324
+ /** Playback time in seconds (only present if restorePlaybackPosition config is enabled) */
1325
+ playbackTime?: number;
1326
+ /** Video ID that was playing (only present if restorePlaybackPosition config is enabled) */
1327
+ currentVideoId?: string;
1328
+ /** Captured video frame at playback position (base64 JPEG, only present if restorePlaybackPosition is enabled) */
1329
+ restoreFrame?: string;
1146
1330
  }
1147
1331
  /**
1148
- * ResourceGovernor configuration
1332
+ * LifecycleManager configuration
1149
1333
  */
1150
- interface ResourceConfig {
1151
- /** Maximum video DOM nodes (default: 3) */
1152
- maxAllocations?: number;
1153
- /** Focus debounce time in ms (default: 150) */
1154
- focusDebounceMs?: number;
1155
- /** Prefetch configuration overrides by network type */
1156
- prefetch?: Partial<Record<NetworkType, Partial<PrefetchConfig>>>;
1157
- /** Scroll thrashing configuration */
1158
- scrollThrashing?: Partial<ScrollThrashingConfig>;
1159
- /** Network adapter for detecting connection type */
1160
- networkAdapter?: INetworkAdapter;
1161
- /** Video loader adapter for preloading video data */
1162
- videoLoader?: IVideoLoader;
1163
- /** Poster loader adapter for preloading thumbnails */
1164
- posterLoader?: IPosterLoader;
1334
+ interface LifecycleConfig {
1335
+ /** Storage adapter for persistence */
1336
+ storage?: ISessionStorage;
1165
1337
  /** Logger adapter */
1166
1338
  logger?: ILogger;
1339
+ /** Snapshot expiry time in ms (default: 24 hours) */
1340
+ snapshotExpiryMs?: number;
1341
+ /** Revalidation threshold in ms (default: 5 minutes) */
1342
+ revalidationThresholdMs?: number;
1343
+ /** Auto-save on visibility change (default: true) */
1344
+ autoSaveOnHidden?: boolean;
1345
+ /**
1346
+ * Enable restoring video playback position (default: false)
1347
+ * When enabled, saves and restores the exact playback time (seconds)
1348
+ * so video can seek() to the exact position user was watching
1349
+ */
1350
+ restorePlaybackPosition?: boolean;
1351
+ /** SDK version for compatibility */
1352
+ version?: string;
1167
1353
  }
1168
1354
  /**
1169
- * Default prefetch configuration by network type
1170
- */
1171
- declare const DEFAULT_PREFETCH_CONFIG: Record<NetworkType, PrefetchConfig>;
1172
- /**
1173
- * Default resource configuration
1355
+ * Default lifecycle configuration
1174
1356
  */
1175
- declare const DEFAULT_RESOURCE_CONFIG: Required<Omit<ResourceConfig, 'networkAdapter' | 'videoLoader' | 'posterLoader' | 'logger' | 'prefetch' | 'scrollThrashing'>> & {
1176
- prefetch: Record<NetworkType, PrefetchConfig>;
1177
- scrollThrashing: ScrollThrashingConfig;
1178
- };
1357
+ declare const DEFAULT_LIFECYCLE_CONFIG: Required<Omit<LifecycleConfig, 'storage' | 'logger'>>;
1179
1358
  /**
1180
- * Resource events for external listening
1359
+ * Lifecycle events for external listening
1181
1360
  */
1182
- type ResourceEvent = {
1183
- type: 'allocationChange';
1184
- toMount: number[];
1185
- toUnmount: number[];
1361
+ type LifecycleEvent = {
1362
+ type: 'saveStart';
1186
1363
  } | {
1187
- type: 'focusChange';
1188
- index: number;
1189
- previousIndex: number;
1364
+ type: 'saveComplete';
1365
+ timestamp: number;
1190
1366
  } | {
1191
- type: 'networkChange';
1192
- networkType: NetworkType;
1367
+ type: 'saveFailed';
1368
+ error: Error;
1193
1369
  } | {
1194
- type: 'prefetchRequest';
1195
- indices: number[];
1370
+ type: 'restoreStart';
1371
+ } | {
1372
+ type: 'restoreComplete';
1373
+ result: RestoreResult;
1374
+ } | {
1375
+ type: 'visibilityChange';
1376
+ state: DocumentVisibilityState;
1196
1377
  };
1197
1378
  /**
1198
- * Resource event listener
1379
+ * Lifecycle event listener
1199
1380
  */
1200
- type ResourceEventListener = (event: ResourceEvent) => void;
1381
+ type LifecycleEventListener = (event: LifecycleEvent) => void;
1201
1382
 
1202
1383
  /**
1203
- * Video source getter function type
1204
- * ResourceGovernor needs this to get video sources for preloading
1205
- */
1206
- type VideoSourceGetter = (index: number) => {
1207
- id: string;
1208
- source: VideoSource;
1209
- poster?: string;
1210
- } | null;
1211
- /**
1212
- * ResourceGovernor - Manages video DOM allocation and prefetch strategy
1384
+ * LifecycleManager - Handles session persistence and restoration
1213
1385
  *
1214
- * Features:
1215
- * - Sliding window allocation (max 3 video DOM nodes)
1216
- * - Focus debouncing for smooth swipe
1217
- * - Network-aware prefetch strategy
1218
- * - Event system for UI synchronization
1386
+ * Strategy: "State Persistence, DOM Destruction"
1387
+ * - Save snapshot to storage when user leaves
1388
+ * - Restore state from storage when user returns
1389
+ * - Video DOM nodes are destroyed to free RAM
1219
1390
  *
1220
- * Memory Strategy:
1221
- * - Only 3 video elements exist in DOM at any time
1222
- * - Sliding window: [previous, current, next]
1223
- * - When user scrolls, we unmount far elements and mount new ones
1391
+ * Features:
1392
+ * - Auto-save on visibility change (optional)
1393
+ * - Snapshot expiry (24 hours default)
1394
+ * - Stale data detection for revalidation
1395
+ * - Event system for UI coordination
1224
1396
  *
1225
1397
  * @example
1226
1398
  * ```typescript
1227
- * const governor = new ResourceGovernor({ maxAllocations: 3 });
1228
- *
1229
- * // Initialize with feed size
1230
- * governor.setTotalItems(20);
1231
- * governor.activate();
1232
- *
1233
- * // Handle scroll/swipe
1234
- * governor.setFocusedIndex(5); // Debounced
1399
+ * const lifecycle = new LifecycleManager({ storage, logger });
1235
1400
  *
1236
- * // Listen to allocation changes
1237
- * governor.addEventListener((event) => {
1238
- * if (event.type === 'allocationChange') {
1239
- * event.toMount.forEach(i => mountVideoAt(i));
1240
- * event.toUnmount.forEach(i => unmountVideoAt(i));
1401
+ * // Initialize and attempt restore
1402
+ * const result = await lifecycle.initialize();
1403
+ * if (result.success) {
1404
+ * feedManager.restoreFromSnapshot(result.snapshot);
1405
+ * if (result.needsRevalidation) {
1406
+ * feedManager.revalidate();
1241
1407
  * }
1408
+ * }
1409
+ *
1410
+ * // Save snapshot before leaving
1411
+ * lifecycle.saveSnapshot({
1412
+ * items: feedManager.getVideos(),
1413
+ * cursor: feedManager.store.getState().cursor,
1414
+ * focusedIndex: resourceGovernor.store.getState().focusedIndex,
1242
1415
  * });
1243
- * ```
1244
- */
1245
- declare class ResourceGovernor {
1246
- /** Zustand vanilla store - Single Source of Truth */
1247
- readonly store: StoreApi<ResourceState>;
1248
- /** Resolved configuration */
1249
- private readonly config;
1250
- /** Network adapter */
1251
- private readonly networkAdapter?;
1252
- /** Video loader adapter for preloading video data */
1253
- private readonly videoLoader?;
1254
- /** Poster loader adapter for preloading thumbnails */
1255
- private readonly posterLoader?;
1256
- /** Logger adapter */
1257
- private readonly logger?;
1258
- /** Event listeners */
1259
- private readonly eventListeners;
1260
- /** Focus debounce timer */
1261
- private focusDebounceTimer;
1262
- /** Pending focused index (before debounce completes) */
1263
- private pendingFocusedIndex;
1264
- /** Network change unsubscribe function */
1265
- private networkUnsubscribe?;
1266
- /** Video source getter (injected via setVideoSourceGetter) */
1267
- private videoSourceGetter?;
1268
- /** Scroll thrashing detection - timestamps of recent focus changes */
1269
- private focusChangeTimestamps;
1270
- /** Scroll thrashing cooldown timer */
1271
- private thrashingCooldownTimer;
1272
- constructor(config?: ResourceConfig);
1273
- /**
1274
- * Activate the resource governor
1275
- * Starts network monitoring and performs initial allocation
1276
- */
1277
- activate(): Promise<void>;
1416
+ * ```
1417
+ */
1418
+ declare class LifecycleManager {
1419
+ /** Zustand vanilla store - Single Source of Truth */
1420
+ readonly store: StoreApi<LifecycleState>;
1421
+ /** Resolved configuration */
1422
+ private readonly config;
1423
+ /** Storage adapter */
1424
+ private readonly storage?;
1425
+ /** Logger adapter */
1426
+ private readonly logger?;
1427
+ /** Event listeners */
1428
+ private readonly eventListeners;
1429
+ /** Visibility change handler reference (for cleanup) */
1430
+ private visibilityHandler?;
1431
+ /** Pending save data (for debouncing) */
1432
+ private pendingSaveData?;
1433
+ constructor(config?: LifecycleConfig);
1278
1434
  /**
1279
- * Deactivate the resource governor
1280
- * Stops network monitoring and clears allocations
1435
+ * Initialize lifecycle manager and attempt to restore session
1281
1436
  */
1282
- deactivate(): void;
1437
+ initialize(): Promise<RestoreResult>;
1283
1438
  /**
1284
- * Destroy the resource governor
1439
+ * Destroy lifecycle manager and cleanup
1285
1440
  */
1286
1441
  destroy(): void;
1287
1442
  /**
1288
- * Set total number of items in feed
1289
- */
1290
- setTotalItems(count: number): void;
1291
- /**
1292
- * Set focused index with debouncing
1293
- * This is called during scroll/swipe
1294
- */
1295
- setFocusedIndex(index: number): void;
1296
- /**
1297
- * Set focused index immediately (skip debounce)
1298
- * Use this for programmatic navigation, not scroll
1299
- */
1300
- setFocusedIndexImmediate(index: number): void;
1301
- /**
1302
- * Request allocation for given indices
1303
- * Returns what needs to be mounted/unmounted
1304
- */
1305
- requestAllocation(indices: number[]): AllocationResult;
1306
- /**
1307
- * Get current active allocations
1443
+ * Restore session from storage
1308
1444
  */
1309
- getActiveAllocations(): number[];
1445
+ restoreSession(): Promise<RestoreResult>;
1310
1446
  /**
1311
- * Check if an index is currently allocated
1447
+ * Save session snapshot to storage
1448
+ *
1449
+ * @param data - Snapshot data to save
1450
+ * @param data.playbackTime - Current video playback position (only saved if restorePlaybackPosition config is enabled)
1451
+ * @param data.currentVideoId - Current video ID (only saved if restorePlaybackPosition config is enabled)
1452
+ * @param data.restoreFrame - Captured video frame at playback position (only saved if restorePlaybackPosition is enabled)
1312
1453
  */
1313
- isAllocated(index: number): boolean;
1454
+ saveSnapshot(data: {
1455
+ items: ContentItem[];
1456
+ cursor: string | null;
1457
+ focusedIndex: number;
1458
+ scrollPosition?: number;
1459
+ /** Current video playback time in seconds (only used when restorePlaybackPosition is enabled) */
1460
+ playbackTime?: number;
1461
+ /** Current video ID (only used when restorePlaybackPosition is enabled) */
1462
+ currentVideoId?: string;
1463
+ /** Captured video frame at playback position (only used when restorePlaybackPosition is enabled) */
1464
+ restoreFrame?: string;
1465
+ }): Promise<boolean>;
1314
1466
  /**
1315
- * Get current network type
1467
+ * Clear saved snapshot
1316
1468
  */
1317
- getNetworkType(): NetworkType;
1469
+ clearSnapshot(): Promise<void>;
1318
1470
  /**
1319
- * Get prefetch configuration for current network
1471
+ * Mark that pending data should be saved (for use with debouncing)
1472
+ *
1473
+ * @param data - Data to save when flush is called
1320
1474
  */
1321
- getPrefetchConfig(): PrefetchConfig;
1475
+ setPendingSave(data: Omit<SessionSnapshot, 'savedAt' | 'version'>): void;
1322
1476
  /**
1323
- * Set video source getter function
1324
- * This is called by SDK to provide video data for preloading
1325
- *
1326
- * @param getter - Function that returns video info for a given index
1477
+ * Check if restorePlaybackPosition config is enabled
1478
+ * SDK can use this to decide whether to collect playbackTime
1327
1479
  */
1328
- setVideoSourceGetter(getter: VideoSourceGetter): void;
1480
+ isPlaybackPositionRestoreEnabled(): boolean;
1329
1481
  /**
1330
- * Check if preloading is currently throttled due to scroll thrashing
1482
+ * Flush pending save (called on visibility hidden)
1331
1483
  */
1332
- isPreloadThrottled(): boolean;
1484
+ flushPendingSave(): Promise<void>;
1333
1485
  /**
1334
- * Get indices currently being preloaded
1486
+ * Handle visibility state change
1335
1487
  */
1336
- getPreloadingIndices(): number[];
1488
+ onVisibilityChange(state: DocumentVisibilityState): void;
1337
1489
  /**
1338
- * Manually trigger preload for specific indices
1339
- * Respects throttling and network conditions
1490
+ * Get current visibility state
1340
1491
  */
1341
- triggerPreload(indices: number[]): Promise<void>;
1492
+ getVisibilityState(): DocumentVisibilityState;
1342
1493
  /**
1343
1494
  * Add event listener
1344
1495
  */
1345
- addEventListener(listener: ResourceEventListener): () => void;
1496
+ addEventListener(listener: LifecycleEventListener): () => void;
1346
1497
  /**
1347
1498
  * Remove event listener
1348
1499
  */
1349
- removeEventListener(listener: ResourceEventListener): void;
1350
- /**
1351
- * Initialize network detection
1352
- */
1353
- private initializeNetwork;
1354
- /**
1355
- * Perform allocation for a given focused index
1356
- */
1357
- private performAllocation;
1358
- /**
1359
- * Update prefetch queue based on current state
1360
- */
1361
- private updatePrefetchQueue;
1500
+ removeEventListener(listener: LifecycleEventListener): void;
1362
1501
  /**
1363
- * Track focus change timestamp for scroll thrashing detection
1502
+ * Setup visibility change listener
1364
1503
  */
1365
- private trackFocusChange;
1504
+ private setupVisibilityListener;
1366
1505
  /**
1367
- * Cancel all in-progress preloads
1506
+ * Validate snapshot data
1368
1507
  */
1369
- private cancelAllPreloads;
1508
+ private validateSnapshot;
1370
1509
  /**
1371
- * Execute video preloading for given indices
1510
+ * Check if snapshot is stale (needs revalidation)
1372
1511
  */
1373
- private executePreload;
1512
+ private isSnapshotStale;
1374
1513
  /**
1375
- * Execute poster preloading for given indices
1514
+ * Create session snapshot from data
1376
1515
  */
1377
- private executePreloadPosters;
1516
+ private createSnapshot;
1378
1517
  /**
1379
1518
  * Emit event to all listeners
1380
1519
  */
@@ -1504,33 +1643,6 @@ type OptimisticEventListener = (event: OptimisticEvent) => void;
1504
1643
 
1505
1644
  /**
1506
1645
  * OptimisticManager - Handles optimistic UI updates with rollback
1507
- *
1508
- * Pattern:
1509
- * 1. User action (like) -> Update UI immediately
1510
- * 2. Call API in background
1511
- * 3. On success -> Keep UI state, remove pending action
1512
- * 4. On error -> Rollback to previous state
1513
- *
1514
- * Features:
1515
- * - Immediate UI feedback
1516
- * - Automatic rollback on failure
1517
- * - Retry queue for failed actions
1518
- * - Prevents duplicate pending actions
1519
- *
1520
- * @example
1521
- * ```typescript
1522
- * const optimistic = new OptimisticManager({
1523
- * interaction,
1524
- * feedManager,
1525
- * logger,
1526
- * });
1527
- *
1528
- * // User taps like button
1529
- * await optimistic.like(videoId);
1530
- *
1531
- * // User taps follow button
1532
- * await optimistic.follow(videoId);
1533
- * ```
1534
1646
  */
1535
1647
  declare class OptimisticManager {
1536
1648
  /** Zustand vanilla store - Single Source of Truth */
@@ -1539,7 +1651,7 @@ declare class OptimisticManager {
1539
1651
  private readonly config;
1540
1652
  /** Interaction adapter */
1541
1653
  private readonly interaction?;
1542
- /** Feed manager for updating video state */
1654
+ /** Feed manager for updating item state */
1543
1655
  private readonly feedManager?;
1544
1656
  /** Logger adapter */
1545
1657
  private readonly logger?;
@@ -1547,137 +1659,65 @@ declare class OptimisticManager {
1547
1659
  private readonly eventListeners;
1548
1660
  /** Retry timer */
1549
1661
  private retryTimer;
1550
- /** Debounce timers for like/unlike per video */
1662
+ /** Debounce timers for like/unlike per item */
1551
1663
  private readonly likeDebounceTimers;
1664
+ /** Intended like state while debouncing (itemId -> isLiked) */
1665
+ private readonly intendedLikeState;
1552
1666
  /** Debounce delay in ms */
1553
1667
  private readonly debounceDelay;
1554
1668
  constructor(config?: OptimisticConfig);
1555
1669
  /**
1556
- * Like a video with optimistic update
1670
+ * Like an item with optimistic update
1557
1671
  */
1558
- like(videoId: string): Promise<boolean>;
1672
+ like(itemId: string): Promise<boolean>;
1559
1673
  /**
1560
- * Unlike a video with optimistic update
1674
+ * Unlike an item with optimistic update
1561
1675
  */
1562
- unlike(videoId: string): Promise<boolean>;
1676
+ unlike(itemId: string): Promise<boolean>;
1563
1677
  /**
1564
- * Toggle like state with DEBOUNCE (like if not liked, unlike if liked)
1565
- *
1566
- * This method:
1567
- * 1. Updates UI immediately (optimistic)
1568
- * 2. Debounces API call - only sends after user stops clicking
1569
- * 3. Sends final state to API after debounce delay
1570
- *
1571
- * Perfect for rapid tapping like TikTok/Instagram behavior.
1678
+ * Toggle like state with DEBOUNCE
1572
1679
  */
1573
- toggleLike(videoId: string): void;
1680
+ toggleLike(itemId: string): void;
1574
1681
  /**
1575
- * Execute the actual API call after debounce
1576
- * Reads current state from FeedManager to get final intended state
1682
+ * Execute API call after debounce delay
1577
1683
  */
1578
1684
  private executeDebouncedLikeApi;
1579
1685
  /**
1580
- * @deprecated Use toggleLike() instead - it now includes debounce
1581
- * Legacy toggle that waits for API response
1686
+ * Handle errors from debounced API calls
1582
1687
  */
1583
- toggleLikeSync(videoId: string): Promise<boolean>;
1688
+ private handleDebouncedApiError;
1584
1689
  /**
1585
- * Follow a video author with optimistic update
1690
+ * Remove a pending action by ID
1586
1691
  */
1587
- follow(videoId: string): Promise<boolean>;
1588
- /**
1589
- * Unfollow a video author with optimistic update
1590
- */
1591
- unfollow(videoId: string): Promise<boolean>;
1692
+ private removePendingAction;
1592
1693
  /**
1593
1694
  * Toggle follow state
1594
1695
  */
1595
- toggleFollow(videoId: string): Promise<boolean>;
1696
+ toggleFollow(itemId: string): Promise<boolean>;
1697
+ follow(itemId: string): Promise<boolean>;
1698
+ unfollow(itemId: string): Promise<boolean>;
1699
+ addEventListener(listener: OptimisticEventListener): () => void;
1700
+ removeEventListener(listener: OptimisticEventListener): void;
1596
1701
  /**
1597
- * Get all pending actions
1702
+ * Reset all optimistic state
1598
1703
  */
1704
+ reset(): void;
1599
1705
  getPendingActions(): PendingAction[];
1600
- /**
1601
- * Check if there's a pending action for a video
1602
- * Only returns true for actions with status 'pending' (not 'failed')
1603
- */
1604
- hasPendingAction(videoId: string, type?: ActionType): boolean;
1605
- /**
1606
- * Get failed actions queue
1607
- */
1706
+ hasPendingAction(itemId: string, type?: ActionType): boolean;
1608
1707
  getFailedQueue(): PendingAction[];
1609
- /**
1610
- * Manually retry failed actions
1611
- */
1612
1708
  retryFailed(): Promise<void>;
1613
- /**
1614
- * Clear all failed actions
1615
- */
1616
1709
  clearFailed(): void;
1617
- /**
1618
- * Reset manager state
1619
- */
1620
- reset(): void;
1621
- /**
1622
- * Destroy manager and cleanup
1623
- */
1624
1710
  destroy(): void;
1625
- /**
1626
- * Add event listener
1627
- */
1628
- addEventListener(listener: OptimisticEventListener): () => void;
1629
- /**
1630
- * Remove event listener
1631
- */
1632
- removeEventListener(listener: OptimisticEventListener): void;
1633
- /**
1634
- * Perform an optimistic action
1635
- */
1711
+ private emit;
1636
1712
  private performAction;
1637
- /**
1638
- * Create rollback data based on action type
1639
- */
1640
1713
  private createRollbackData;
1641
- /**
1642
- * Apply optimistic update to feed
1643
- */
1644
1714
  private applyOptimisticUpdate;
1645
- /**
1646
- * Apply rollback
1647
- */
1648
1715
  private applyRollback;
1649
- /**
1650
- * Add pending action to store
1651
- */
1652
1716
  private addPendingAction;
1653
- /**
1654
- * Mark action as success
1655
- */
1656
1717
  private markActionSuccess;
1657
- /**
1658
- * Mark action as failed
1659
- */
1660
1718
  private markActionFailed;
1661
- /**
1662
- * Retry a failed action
1663
- */
1664
1719
  private retryAction;
1665
- /**
1666
- * Execute API call based on action type
1667
- */
1668
- private executeApiCall;
1669
- /**
1670
- * Schedule retry of failed actions
1671
- */
1672
1720
  private scheduleRetry;
1673
- /**
1674
- * Cancel retry timer
1675
- */
1676
- private cancelRetryTimer;
1677
- /**
1678
- * Emit event to all listeners
1679
- */
1680
- private emitEvent;
1681
1721
  }
1682
1722
 
1683
1723
  /**
@@ -1914,4 +1954,152 @@ declare class CommentManager {
1914
1954
  private createError;
1915
1955
  }
1916
1956
 
1917
- export { type ActionType, type AllocationResult, type CommentError, CommentManager, type CommentManagerConfig, type CommentManagerState, 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, type FeedConfig, type FeedError, FeedManager, type FeedState, type LifecycleConfig, type LifecycleEvent, type LifecycleEventListener, LifecycleManager, type LifecycleState, type NetworkType, type OptimisticComment, type OptimisticConfig, type OptimisticEvent, type OptimisticEventListener, OptimisticManager, type OptimisticReply, type OptimisticState, type PendingAction, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PrefetchCacheConfig, type PrefetchConfig, type ResourceConfig, type ResourceEvent, type ResourceEventListener, ResourceGovernor, type ResourceState, type RestoreResult, type VideoCommentState, calculatePrefetchIndices, calculateWindowIndices, canPause, canPlay, canSeek, computeAllocationChanges, createInitialCommentState, createInitialVideoCommentState, isActiveState, isValidTransition, mapNetworkType };
1957
+ /**
1958
+ * Playlist state for zustand store
1959
+ */
1960
+ interface PlaylistState {
1961
+ /** Currently loaded playlist metadata */
1962
+ playlist: PlaylistData | null;
1963
+ /** Current active item index */
1964
+ currentIndex: number;
1965
+ /** List of items in the playlist (shortcut to playlist.items) */
1966
+ items: ContentItem[];
1967
+ /** Loading state */
1968
+ loading: boolean;
1969
+ /** Error state */
1970
+ error: Error | null;
1971
+ }
1972
+ /**
1973
+ * Configuration for PlaylistManager
1974
+ */
1975
+ interface PlaylistConfig {
1976
+ /**
1977
+ * Number of items to keep full metadata for around the current index
1978
+ * @default 10
1979
+ */
1980
+ metadataWindowSize?: number;
1981
+ }
1982
+ /**
1983
+ * Actions for PlaylistManager
1984
+ */
1985
+ interface PlaylistActions {
1986
+ /** Load a playlist by ID */
1987
+ loadPlaylist: (id: string) => Promise<void>;
1988
+ /** Set a playlist directly */
1989
+ setPlaylist: (playlist: PlaylistData) => void;
1990
+ /** Navigate to next item */
1991
+ next: () => void;
1992
+ /** Navigate to previous item */
1993
+ prev: () => void;
1994
+ /** Jump to specific index */
1995
+ jumpTo: (index: number) => void;
1996
+ /** Reset state */
1997
+ reset: () => void;
1998
+ }
1999
+ /**
2000
+ * State for PlaylistCollectionManager
2001
+ */
2002
+ interface PlaylistCollectionState {
2003
+ /** Array of playlist summaries */
2004
+ playlists: PlaylistSummary[];
2005
+ /** Whether loading is in progress */
2006
+ loading: boolean;
2007
+ /** Current pagination cursor */
2008
+ cursor: string | null;
2009
+ /** Whether more items can be loaded */
2010
+ hasMore: boolean;
2011
+ /** Error state */
2012
+ error: Error | null;
2013
+ }
2014
+
2015
+ /**
2016
+ * PlaylistManager - Manages playlist state and navigation
2017
+ *
2018
+ * Features:
2019
+ * - Tracks current playlist and active index
2020
+ * - Provides navigation actions (next, prev, jumpTo)
2021
+ * - Uses vanilla Zustand store for state management
2022
+ */
2023
+ declare class PlaylistManager {
2024
+ private readonly dataSource?;
2025
+ private readonly governor?;
2026
+ /** Zustand vanilla store */
2027
+ readonly store: StoreApi<PlaylistState>;
2028
+ /** Resolved configuration */
2029
+ private readonly config;
2030
+ /**
2031
+ * Internal cache of full metadata items.
2032
+ * Items in store.items may be minified to save memory.
2033
+ */
2034
+ private fullMetadataItems;
2035
+ /** Unsubscribe from governor events */
2036
+ private governorUnsubscribe?;
2037
+ constructor(dataSource?: IPlaylistDataSource | undefined, config?: PlaylistConfig, governor?: ResourceGovernor | undefined);
2038
+ /**
2039
+ * Load a playlist by ID
2040
+ */
2041
+ loadPlaylist(id: string): Promise<void>;
2042
+ /**
2043
+ * Set playlist data directly
2044
+ */
2045
+ setPlaylist(playlist: PlaylistData): void;
2046
+ /**
2047
+ * Navigate to next item
2048
+ */
2049
+ next(): void;
2050
+ /**
2051
+ * Navigate to previous item
2052
+ */
2053
+ prev(): void;
2054
+ /**
2055
+ * Jump to specific index
2056
+ */
2057
+ jumpTo(index: number): void;
2058
+ /**
2059
+ * Reset state
2060
+ */
2061
+ reset(): void;
2062
+ /**
2063
+ * Destroy the manager
2064
+ */
2065
+ destroy(): void;
2066
+ /**
2067
+ * Update the sliding window of full metadata items.
2068
+ * Items outside the window are minified to save memory.
2069
+ */
2070
+ private updateMetadataWindow;
2071
+ /**
2072
+ * Create a minified version of a ContentItem to save memory.
2073
+ * Keeps only essential fields for identification and basic UI.
2074
+ */
2075
+ private minifyItem;
2076
+ }
2077
+
2078
+ /**
2079
+ * PlaylistCollectionManager - Manages the discovery of playlists.
2080
+ *
2081
+ * Responsibilities:
2082
+ * - Fetching playlist summaries from DataSource
2083
+ * - Handling pagination and cursor management
2084
+ * - Maintaining loading and error states
2085
+ */
2086
+ declare class PlaylistCollectionManager {
2087
+ private readonly dataSource?;
2088
+ /** Zustand vanilla store */
2089
+ readonly store: StoreApi<PlaylistCollectionState>;
2090
+ constructor(dataSource?: IPlaylistDataSource | undefined);
2091
+ /**
2092
+ * Load more playlists (pagination)
2093
+ */
2094
+ loadMore(): Promise<void>;
2095
+ /**
2096
+ * Refresh the collection (reset and re-fetch)
2097
+ */
2098
+ refresh(): Promise<void>;
2099
+ /**
2100
+ * Reset the store to initial state
2101
+ */
2102
+ reset(): void;
2103
+ }
2104
+
2105
+ export { type ActionType, type AllocationResult, type CommentError, CommentManager, type CommentManagerConfig, type CommentManagerState, 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, type FeedConfig, type FeedError, FeedManager, type FeedState, type LifecycleConfig, type LifecycleEvent, type LifecycleEventListener, LifecycleManager, type LifecycleState, type NetworkType, type OptimisticComment, type OptimisticConfig, type OptimisticEvent, type OptimisticEventListener, OptimisticManager, type OptimisticReply, type OptimisticState, type PendingAction, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PlaylistActions, PlaylistCollectionManager, type PlaylistCollectionState, PlaylistManager, type PlaylistState, type PrefetchCacheConfig, type PrefetchConfig, type ResourceConfig, type ResourceEvent, type ResourceEventListener, ResourceGovernor, type ResourceState, type RestoreResult, type VideoCommentState, calculatePrefetchIndices, calculateWindowIndices, canPause, canPlay, canSeek, computeAllocationChanges, createInitialCommentState, createInitialVideoCommentState, isActiveState, isValidTransition, mapNetworkType };