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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +788 -625
  2. package/dist/index.js +780 -459
  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,18 @@ 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);
173
495
  /** Static memory cache for explicit prefetching */
174
496
  private static globalMemoryCache;
175
497
  /**
@@ -189,6 +511,21 @@ declare class FeedManager {
189
511
  * Clear prefetch cache
190
512
  */
191
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;
192
529
  /**
193
530
  * Load initial feed data
194
531
  *
@@ -201,7 +538,9 @@ declare class FeedManager {
201
538
  * - If a request for the same cursor is already in-flight, returns the existing Promise
202
539
  * - Prevents duplicate API calls from rapid UI interactions
203
540
  */
204
- loadInitial(): Promise<void>;
541
+ loadInitial(options?: {
542
+ replace?: boolean;
543
+ }): Promise<void>;
205
544
  /**
206
545
  * Internal: Execute load initial logic
207
546
  */
@@ -226,18 +565,59 @@ declare class FeedManager {
226
565
  */
227
566
  revalidate(): Promise<void>;
228
567
  /**
229
- * Get a video by ID
230
- * 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
231
591
  */
232
592
  getVideo(id: string): VideoItem | undefined;
233
593
  /**
234
- * Get ordered list of videos
594
+ * @deprecated Use getItems instead
235
595
  */
236
596
  getVideos(): VideoItem[];
237
597
  /**
238
- * Update a video in the feed (for optimistic updates)
598
+ * @deprecated Use updateItem instead
239
599
  */
240
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;
241
621
  /**
242
622
  * Check if data is stale and needs revalidation
243
623
  */
@@ -260,20 +640,37 @@ declare class FeedManager {
260
640
  * Used by LifecycleManager to restore state without API call.
261
641
  * This bypasses normal data flow for state restoration.
262
642
  *
263
- * @param items - Video items from snapshot
643
+ * @param items - Content items from snapshot
264
644
  * @param cursor - Pagination cursor from snapshot
265
645
  * @param options - Additional hydration options
266
646
  */
267
- hydrateFromSnapshot(items: VideoItem[], cursor: string | null, options?: {
647
+ hydrateFromSnapshot(items: ContentItem[], cursor: string | null, options?: {
268
648
  /** Whether to mark data as stale for background revalidation */
269
649
  markAsStale?: boolean;
270
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;
271
668
  /**
272
669
  * Update prefetch cache with current feed tail
273
670
  * Called automatically after loadInitial() and loadMore()
274
671
  *
275
- * Strategy: Cache the LAST N videos (tail of feed)
276
- * 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
277
674
  */
278
675
  updatePrefetchCache(): void;
279
676
  /**
@@ -290,21 +687,37 @@ declare class FeedManager {
290
687
  * Marks data as stale to trigger background revalidation
291
688
  */
292
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;
293
706
  /**
294
707
  * Clear prefetch cache
295
708
  * Call when user logs out or data should be invalidated
296
709
  */
297
710
  clearPrefetchCache(): Promise<void>;
298
711
  /**
299
- * Evict videos that user has scrolled past from prefetch cache
712
+ * Evict items that user has scrolled past from prefetch cache
300
713
  * Called when user's focusedIndex changes
301
714
  *
302
- * Strategy: Remove all videos at or before current position
303
- * 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
304
717
  *
305
- * @param currentIndex - Current focused video index in feed
718
+ * @param currentIndex - Current focused item index in feed
306
719
  */
307
- evictViewedVideosFromCache(currentIndex: number): Promise<void>;
720
+ evictViewedItemsFromCache(currentIndex: number): Promise<void>;
308
721
  /**
309
722
  * Get prefetch cache configuration (for external access)
310
723
  */
@@ -314,15 +727,15 @@ declare class FeedManager {
314
727
  */
315
728
  private fetchWithRetry;
316
729
  /**
317
- * Add videos with deduplication
730
+ * Add items with deduplication
318
731
  * Triggers garbage collection if cache exceeds maxCacheSize
319
732
  */
320
- private addVideos;
733
+ private addItems;
321
734
  /**
322
- * Merge videos (for SWR revalidation)
323
- * 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
324
737
  */
325
- private mergeVideos;
738
+ private mergeItems;
326
739
  /**
327
740
  * Handle and categorize errors
328
741
  */
@@ -339,9 +752,9 @@ declare class FeedManager {
339
752
  * Run garbage collection using LRU (Least Recently Used) policy
340
753
  *
341
754
  * When cache size exceeds maxCacheSize:
342
- * 1. Sort videos by last access time (oldest first)
343
- * 2. Evict oldest videos until cache is within limit
344
- * 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)
345
758
  *
346
759
  * @returns Number of evicted items
347
760
  */
@@ -863,543 +1276,244 @@ declare function isValidTransition(from: PlayerStatus, to: PlayerStatus): boolea
863
1276
  /**
864
1277
  * Check if player is in a "active" state (can receive playback commands)
865
1278
  */
866
- declare function isActiveState(status: PlayerStatus): boolean;
867
- /**
868
- * Check if player can start playback
869
- */
870
- declare function canPlay(status: PlayerStatus): boolean;
871
- /**
872
- * Check if player can pause
873
- */
874
- declare function canPause(status: PlayerStatus): boolean;
875
- /**
876
- * Check if player can seek
877
- */
878
- declare function canSeek(status: PlayerStatus): boolean;
879
-
880
- /**
881
- * Lifecycle state for zustand store
882
- */
883
- interface LifecycleState {
884
- /** Whether manager is initialized */
885
- isInitialized: boolean;
886
- /** Whether there's a pending save operation */
887
- isSaving: boolean;
888
- /** Whether there's a pending restore operation */
889
- isRestoring: boolean;
890
- /** Last saved timestamp */
891
- lastSavedAt: number | null;
892
- /** Last restored timestamp */
893
- lastRestoredAt: number | null;
894
- /** Whether the restored snapshot needs revalidation */
895
- needsRevalidation: boolean;
896
- /** Current visibility state */
897
- visibilityState: DocumentVisibilityState;
898
- }
899
- /**
900
- * Restore result
901
- */
902
- interface RestoreResult {
903
- /** Whether restore was successful */
904
- success: boolean;
905
- /** Restored snapshot data (null if no valid snapshot) */
906
- snapshot: SessionSnapshot | null;
907
- /** Whether the restored data is stale and needs revalidation */
908
- needsRevalidation: boolean;
909
- /** Reason for failure (if any) */
910
- reason?: 'no_snapshot' | 'expired' | 'invalid' | 'error';
911
- /** Playback time in seconds (only present if restorePlaybackPosition config is enabled) */
912
- playbackTime?: number;
913
- /** Video ID that was playing (only present if restorePlaybackPosition config is enabled) */
914
- currentVideoId?: string;
915
- /** Captured video frame at playback position (base64 JPEG, only present if restorePlaybackPosition is enabled) */
916
- restoreFrame?: string;
917
- }
918
- /**
919
- * LifecycleManager configuration
920
- */
921
- interface LifecycleConfig {
922
- /** Storage adapter for persistence */
923
- storage?: ISessionStorage;
924
- /** Logger adapter */
925
- logger?: ILogger;
926
- /** Snapshot expiry time in ms (default: 24 hours) */
927
- snapshotExpiryMs?: number;
928
- /** Revalidation threshold in ms (default: 5 minutes) */
929
- revalidationThresholdMs?: number;
930
- /** Auto-save on visibility change (default: true) */
931
- autoSaveOnHidden?: boolean;
932
- /**
933
- * Enable restoring video playback position (default: false)
934
- * When enabled, saves and restores the exact playback time (seconds)
935
- * so video can seek() to the exact position user was watching
936
- */
937
- restorePlaybackPosition?: boolean;
938
- /** SDK version for compatibility */
939
- version?: string;
940
- }
941
- /**
942
- * Default lifecycle configuration
943
- */
944
- declare const DEFAULT_LIFECYCLE_CONFIG: Required<Omit<LifecycleConfig, 'storage' | 'logger'>>;
945
- /**
946
- * Lifecycle events for external listening
947
- */
948
- type LifecycleEvent = {
949
- type: 'saveStart';
950
- } | {
951
- type: 'saveComplete';
952
- timestamp: number;
953
- } | {
954
- type: 'saveFailed';
955
- error: Error;
956
- } | {
957
- type: 'restoreStart';
958
- } | {
959
- type: 'restoreComplete';
960
- result: RestoreResult;
961
- } | {
962
- type: 'visibilityChange';
963
- state: DocumentVisibilityState;
964
- };
965
- /**
966
- * Lifecycle event listener
967
- */
968
- type LifecycleEventListener = (event: LifecycleEvent) => void;
969
-
970
- /**
971
- * LifecycleManager - Handles session persistence and restoration
972
- *
973
- * Strategy: "State Persistence, DOM Destruction"
974
- * - Save snapshot to storage when user leaves
975
- * - Restore state from storage when user returns
976
- * - Video DOM nodes are destroyed to free RAM
977
- *
978
- * Features:
979
- * - Auto-save on visibility change (optional)
980
- * - Snapshot expiry (24 hours default)
981
- * - Stale data detection for revalidation
982
- * - Event system for UI coordination
983
- *
984
- * @example
985
- * ```typescript
986
- * const lifecycle = new LifecycleManager({ storage, logger });
987
- *
988
- * // Initialize and attempt restore
989
- * const result = await lifecycle.initialize();
990
- * if (result.success) {
991
- * feedManager.restoreFromSnapshot(result.snapshot);
992
- * if (result.needsRevalidation) {
993
- * feedManager.revalidate();
994
- * }
995
- * }
996
- *
997
- * // Save snapshot before leaving
998
- * lifecycle.saveSnapshot({
999
- * items: feedManager.getVideos(),
1000
- * cursor: feedManager.store.getState().cursor,
1001
- * focusedIndex: resourceGovernor.store.getState().focusedIndex,
1002
- * });
1003
- * ```
1004
- */
1005
- declare class LifecycleManager {
1006
- /** Zustand vanilla store - Single Source of Truth */
1007
- readonly store: StoreApi<LifecycleState>;
1008
- /** Resolved configuration */
1009
- private readonly config;
1010
- /** Storage adapter */
1011
- private readonly storage?;
1012
- /** Logger adapter */
1013
- private readonly logger?;
1014
- /** Event listeners */
1015
- private readonly eventListeners;
1016
- /** Visibility change handler reference (for cleanup) */
1017
- private visibilityHandler?;
1018
- /** Pending save data (for debouncing) */
1019
- private pendingSaveData?;
1020
- constructor(config?: LifecycleConfig);
1021
- /**
1022
- * Initialize lifecycle manager and attempt to restore session
1023
- */
1024
- initialize(): Promise<RestoreResult>;
1025
- /**
1026
- * Destroy lifecycle manager and cleanup
1027
- */
1028
- destroy(): void;
1029
- /**
1030
- * Restore session from storage
1031
- */
1032
- restoreSession(): Promise<RestoreResult>;
1033
- /**
1034
- * Save session snapshot to storage
1035
- *
1036
- * @param data - Snapshot data to save
1037
- * @param data.playbackTime - Current video playback position (only saved if restorePlaybackPosition config is enabled)
1038
- * @param data.currentVideoId - Current video ID (only saved if restorePlaybackPosition config is enabled)
1039
- * @param data.restoreFrame - Captured video frame at playback position (only saved if restorePlaybackPosition is enabled)
1040
- */
1041
- saveSnapshot(data: {
1042
- items: VideoItem[];
1043
- cursor: string | null;
1044
- focusedIndex: number;
1045
- scrollPosition?: number;
1046
- /** Current video playback time in seconds (only used when restorePlaybackPosition is enabled) */
1047
- playbackTime?: number;
1048
- /** Current video ID (only used when restorePlaybackPosition is enabled) */
1049
- currentVideoId?: string;
1050
- /** Captured video frame at playback position (only used when restorePlaybackPosition is enabled) */
1051
- restoreFrame?: string;
1052
- }): Promise<boolean>;
1053
- /**
1054
- * Clear saved snapshot
1055
- */
1056
- clearSnapshot(): Promise<void>;
1057
- /**
1058
- * Mark that pending data should be saved (for use with debouncing)
1059
- *
1060
- * @param data - Data to save when flush is called
1061
- */
1062
- setPendingSave(data: Omit<SessionSnapshot, 'savedAt' | 'version'>): void;
1063
- /**
1064
- * Check if restorePlaybackPosition config is enabled
1065
- * SDK can use this to decide whether to collect playbackTime
1066
- */
1067
- isPlaybackPositionRestoreEnabled(): boolean;
1068
- /**
1069
- * Flush pending save (called on visibility hidden)
1070
- */
1071
- flushPendingSave(): Promise<void>;
1072
- /**
1073
- * Handle visibility state change
1074
- */
1075
- onVisibilityChange(state: DocumentVisibilityState): void;
1076
- /**
1077
- * Get current visibility state
1078
- */
1079
- getVisibilityState(): DocumentVisibilityState;
1080
- /**
1081
- * Add event listener
1082
- */
1083
- addEventListener(listener: LifecycleEventListener): () => void;
1084
- /**
1085
- * Remove event listener
1086
- */
1087
- removeEventListener(listener: LifecycleEventListener): void;
1088
- /**
1089
- * Setup visibility change listener
1090
- */
1091
- private setupVisibilityListener;
1092
- /**
1093
- * Validate snapshot data
1094
- */
1095
- private validateSnapshot;
1096
- /**
1097
- * Check if snapshot is stale (needs revalidation)
1098
- */
1099
- private isSnapshotStale;
1100
- /**
1101
- * Emit event to all listeners
1102
- */
1103
- private emitEvent;
1104
- }
1105
-
1106
- /**
1107
- * Network type for prefetch strategy
1108
- * Simplified from contracts NetworkType for prefetch decisions
1109
- */
1110
- type NetworkType = 'wifi' | 'cellular' | 'offline';
1279
+ declare function isActiveState(status: PlayerStatus): boolean;
1111
1280
  /**
1112
- * Map contracts NetworkType to simplified NetworkType
1281
+ * Check if player can start playback
1113
1282
  */
1114
- declare function mapNetworkType(type: string): NetworkType;
1283
+ declare function canPlay(status: PlayerStatus): boolean;
1115
1284
  /**
1116
- * Resource state for zustand store
1285
+ * Check if player can pause
1117
1286
  */
1118
- interface ResourceState {
1119
- /** Currently allocated video slot indices (max 3) */
1120
- activeAllocations: Set<number>;
1121
- /** Queue of indices to preload */
1122
- preloadQueue: number[];
1123
- /** Currently focused/playing video index */
1124
- focusedIndex: number;
1125
- /** Current network type */
1126
- networkType: NetworkType;
1127
- /** Total number of items in feed (for bounds checking) */
1128
- totalItems: number;
1129
- /** Whether resource governor is active */
1130
- isActive: boolean;
1131
- /** Whether preloading is throttled due to scroll thrashing */
1132
- isThrottled: boolean;
1133
- /** Indices currently being preloaded */
1134
- preloadingIndices: Set<number>;
1135
- }
1287
+ declare function canPause(status: PlayerStatus): boolean;
1136
1288
  /**
1137
- * Allocation result returned by requestAllocation
1289
+ * Check if player can seek
1138
1290
  */
1139
- interface AllocationResult {
1140
- /** Indices that should mount video elements */
1141
- toMount: number[];
1142
- /** Indices that should unmount video elements */
1143
- toUnmount: number[];
1144
- /** Currently active allocations after this operation */
1145
- activeAllocations: number[];
1146
- /** Whether focus change was successful */
1147
- success: boolean;
1148
- }
1291
+ declare function canSeek(status: PlayerStatus): boolean;
1292
+
1149
1293
  /**
1150
- * Prefetch configuration by network type
1294
+ * Lifecycle state for zustand store
1151
1295
  */
1152
- interface PrefetchConfig {
1153
- /** Number of posters to prefetch ahead (default: wifi=5, cellular=3, offline=1) */
1154
- posterCount: number;
1155
- /** Number of video segments to prefetch (default: wifi=2, cellular=1, offline=0) */
1156
- videoSegmentCount: number;
1157
- /** Whether to prefetch video at all (default: true for wifi/cellular) */
1158
- 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;
1159
1311
  }
1160
1312
  /**
1161
- * Scroll thrashing configuration
1162
- * Prevents excessive preloading when user scrolls too fast
1313
+ * Restore result
1163
1314
  */
1164
- interface ScrollThrashingConfig {
1165
- /** Time window to measure scroll rate (ms) - default: 1000 */
1166
- windowMs: number;
1167
- /** Max focus changes allowed in window before throttling - default: 3 */
1168
- maxChangesInWindow: number;
1169
- /** Cooldown time after throttle before resuming preload (ms) - default: 500 */
1170
- 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;
1171
1330
  }
1172
1331
  /**
1173
- * ResourceGovernor configuration
1332
+ * LifecycleManager configuration
1174
1333
  */
1175
- interface ResourceConfig {
1176
- /** Maximum video DOM nodes (default: 3) */
1177
- maxAllocations?: number;
1178
- /** Focus debounce time in ms (default: 150) */
1179
- focusDebounceMs?: number;
1180
- /** Prefetch configuration overrides by network type */
1181
- prefetch?: Partial<Record<NetworkType, Partial<PrefetchConfig>>>;
1182
- /** Scroll thrashing configuration */
1183
- scrollThrashing?: Partial<ScrollThrashingConfig>;
1184
- /** Network adapter for detecting connection type */
1185
- networkAdapter?: INetworkAdapter;
1186
- /** Video loader adapter for preloading video data */
1187
- videoLoader?: IVideoLoader;
1188
- /** Poster loader adapter for preloading thumbnails */
1189
- posterLoader?: IPosterLoader;
1334
+ interface LifecycleConfig {
1335
+ /** Storage adapter for persistence */
1336
+ storage?: ISessionStorage;
1190
1337
  /** Logger adapter */
1191
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;
1192
1353
  }
1193
1354
  /**
1194
- * Default prefetch configuration by network type
1195
- */
1196
- declare const DEFAULT_PREFETCH_CONFIG: Record<NetworkType, PrefetchConfig>;
1197
- /**
1198
- * Default resource configuration
1355
+ * Default lifecycle configuration
1199
1356
  */
1200
- declare const DEFAULT_RESOURCE_CONFIG: Required<Omit<ResourceConfig, 'networkAdapter' | 'videoLoader' | 'posterLoader' | 'logger' | 'prefetch' | 'scrollThrashing'>> & {
1201
- prefetch: Record<NetworkType, PrefetchConfig>;
1202
- scrollThrashing: ScrollThrashingConfig;
1203
- };
1357
+ declare const DEFAULT_LIFECYCLE_CONFIG: Required<Omit<LifecycleConfig, 'storage' | 'logger'>>;
1204
1358
  /**
1205
- * Resource events for external listening
1359
+ * Lifecycle events for external listening
1206
1360
  */
1207
- type ResourceEvent = {
1208
- type: 'allocationChange';
1209
- toMount: number[];
1210
- toUnmount: number[];
1361
+ type LifecycleEvent = {
1362
+ type: 'saveStart';
1211
1363
  } | {
1212
- type: 'focusChange';
1213
- index: number;
1214
- previousIndex: number;
1364
+ type: 'saveComplete';
1365
+ timestamp: number;
1215
1366
  } | {
1216
- type: 'networkChange';
1217
- networkType: NetworkType;
1367
+ type: 'saveFailed';
1368
+ error: Error;
1218
1369
  } | {
1219
- type: 'prefetchRequest';
1220
- indices: number[];
1370
+ type: 'restoreStart';
1371
+ } | {
1372
+ type: 'restoreComplete';
1373
+ result: RestoreResult;
1374
+ } | {
1375
+ type: 'visibilityChange';
1376
+ state: DocumentVisibilityState;
1221
1377
  };
1222
1378
  /**
1223
- * Resource event listener
1379
+ * Lifecycle event listener
1224
1380
  */
1225
- type ResourceEventListener = (event: ResourceEvent) => void;
1381
+ type LifecycleEventListener = (event: LifecycleEvent) => void;
1226
1382
 
1227
1383
  /**
1228
- * Video source getter function type
1229
- * ResourceGovernor needs this to get video sources for preloading
1230
- */
1231
- type VideoSourceGetter = (index: number) => {
1232
- id: string;
1233
- source: VideoSource;
1234
- poster?: string;
1235
- } | null;
1236
- /**
1237
- * ResourceGovernor - Manages video DOM allocation and prefetch strategy
1384
+ * LifecycleManager - Handles session persistence and restoration
1238
1385
  *
1239
- * Features:
1240
- * - Sliding window allocation (max 3 video DOM nodes)
1241
- * - Focus debouncing for smooth swipe
1242
- * - Network-aware prefetch strategy
1243
- * - 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
1244
1390
  *
1245
- * Memory Strategy:
1246
- * - Only 3 video elements exist in DOM at any time
1247
- * - Sliding window: [previous, current, next]
1248
- * - 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
1249
1396
  *
1250
1397
  * @example
1251
1398
  * ```typescript
1252
- * const governor = new ResourceGovernor({ maxAllocations: 3 });
1253
- *
1254
- * // Initialize with feed size
1255
- * governor.setTotalItems(20);
1256
- * governor.activate();
1257
- *
1258
- * // Handle scroll/swipe
1259
- * governor.setFocusedIndex(5); // Debounced
1399
+ * const lifecycle = new LifecycleManager({ storage, logger });
1260
1400
  *
1261
- * // Listen to allocation changes
1262
- * governor.addEventListener((event) => {
1263
- * if (event.type === 'allocationChange') {
1264
- * event.toMount.forEach(i => mountVideoAt(i));
1265
- * 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();
1266
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,
1267
1415
  * });
1268
- * ```
1269
- */
1270
- declare class ResourceGovernor {
1271
- /** Zustand vanilla store - Single Source of Truth */
1272
- readonly store: StoreApi<ResourceState>;
1273
- /** Resolved configuration */
1274
- private readonly config;
1275
- /** Network adapter */
1276
- private readonly networkAdapter?;
1277
- /** Video loader adapter for preloading video data */
1278
- private readonly videoLoader?;
1279
- /** Poster loader adapter for preloading thumbnails */
1280
- private readonly posterLoader?;
1281
- /** Logger adapter */
1282
- private readonly logger?;
1283
- /** Event listeners */
1284
- private readonly eventListeners;
1285
- /** Focus debounce timer */
1286
- private focusDebounceTimer;
1287
- /** Pending focused index (before debounce completes) */
1288
- private pendingFocusedIndex;
1289
- /** Network change unsubscribe function */
1290
- private networkUnsubscribe?;
1291
- /** Video source getter (injected via setVideoSourceGetter) */
1292
- private videoSourceGetter?;
1293
- /** Scroll thrashing detection - timestamps of recent focus changes */
1294
- private focusChangeTimestamps;
1295
- /** Scroll thrashing cooldown timer */
1296
- private thrashingCooldownTimer;
1297
- constructor(config?: ResourceConfig);
1298
- /**
1299
- * Activate the resource governor
1300
- * Starts network monitoring and performs initial allocation
1301
- */
1302
- 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);
1303
1434
  /**
1304
- * Deactivate the resource governor
1305
- * Stops network monitoring and clears allocations
1435
+ * Initialize lifecycle manager and attempt to restore session
1306
1436
  */
1307
- deactivate(): void;
1437
+ initialize(): Promise<RestoreResult>;
1308
1438
  /**
1309
- * Destroy the resource governor
1439
+ * Destroy lifecycle manager and cleanup
1310
1440
  */
1311
1441
  destroy(): void;
1312
1442
  /**
1313
- * Set total number of items in feed
1314
- */
1315
- setTotalItems(count: number): void;
1316
- /**
1317
- * Set focused index with debouncing
1318
- * This is called during scroll/swipe
1319
- */
1320
- setFocusedIndex(index: number): void;
1321
- /**
1322
- * Set focused index immediately (skip debounce)
1323
- * Use this for programmatic navigation, not scroll
1324
- */
1325
- setFocusedIndexImmediate(index: number): void;
1326
- /**
1327
- * Request allocation for given indices
1328
- * Returns what needs to be mounted/unmounted
1329
- */
1330
- requestAllocation(indices: number[]): AllocationResult;
1331
- /**
1332
- * Get current active allocations
1443
+ * Restore session from storage
1333
1444
  */
1334
- getActiveAllocations(): number[];
1445
+ restoreSession(): Promise<RestoreResult>;
1335
1446
  /**
1336
- * 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)
1337
1453
  */
1338
- 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>;
1339
1466
  /**
1340
- * Get current network type
1467
+ * Clear saved snapshot
1341
1468
  */
1342
- getNetworkType(): NetworkType;
1469
+ clearSnapshot(): Promise<void>;
1343
1470
  /**
1344
- * 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
1345
1474
  */
1346
- getPrefetchConfig(): PrefetchConfig;
1475
+ setPendingSave(data: Omit<SessionSnapshot, 'savedAt' | 'version'>): void;
1347
1476
  /**
1348
- * Set video source getter function
1349
- * This is called by SDK to provide video data for preloading
1350
- *
1351
- * @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
1352
1479
  */
1353
- setVideoSourceGetter(getter: VideoSourceGetter): void;
1480
+ isPlaybackPositionRestoreEnabled(): boolean;
1354
1481
  /**
1355
- * Check if preloading is currently throttled due to scroll thrashing
1482
+ * Flush pending save (called on visibility hidden)
1356
1483
  */
1357
- isPreloadThrottled(): boolean;
1484
+ flushPendingSave(): Promise<void>;
1358
1485
  /**
1359
- * Get indices currently being preloaded
1486
+ * Handle visibility state change
1360
1487
  */
1361
- getPreloadingIndices(): number[];
1488
+ onVisibilityChange(state: DocumentVisibilityState): void;
1362
1489
  /**
1363
- * Manually trigger preload for specific indices
1364
- * Respects throttling and network conditions
1490
+ * Get current visibility state
1365
1491
  */
1366
- triggerPreload(indices: number[]): Promise<void>;
1492
+ getVisibilityState(): DocumentVisibilityState;
1367
1493
  /**
1368
1494
  * Add event listener
1369
1495
  */
1370
- addEventListener(listener: ResourceEventListener): () => void;
1496
+ addEventListener(listener: LifecycleEventListener): () => void;
1371
1497
  /**
1372
1498
  * Remove event listener
1373
1499
  */
1374
- removeEventListener(listener: ResourceEventListener): void;
1375
- /**
1376
- * Initialize network detection
1377
- */
1378
- private initializeNetwork;
1379
- /**
1380
- * Perform allocation for a given focused index
1381
- */
1382
- private performAllocation;
1383
- /**
1384
- * Update prefetch queue based on current state
1385
- */
1386
- private updatePrefetchQueue;
1500
+ removeEventListener(listener: LifecycleEventListener): void;
1387
1501
  /**
1388
- * Track focus change timestamp for scroll thrashing detection
1502
+ * Setup visibility change listener
1389
1503
  */
1390
- private trackFocusChange;
1504
+ private setupVisibilityListener;
1391
1505
  /**
1392
- * Cancel all in-progress preloads
1506
+ * Validate snapshot data
1393
1507
  */
1394
- private cancelAllPreloads;
1508
+ private validateSnapshot;
1395
1509
  /**
1396
- * Execute video preloading for given indices
1510
+ * Check if snapshot is stale (needs revalidation)
1397
1511
  */
1398
- private executePreload;
1512
+ private isSnapshotStale;
1399
1513
  /**
1400
- * Execute poster preloading for given indices
1514
+ * Create session snapshot from data
1401
1515
  */
1402
- private executePreloadPosters;
1516
+ private createSnapshot;
1403
1517
  /**
1404
1518
  * Emit event to all listeners
1405
1519
  */
@@ -1529,33 +1643,6 @@ type OptimisticEventListener = (event: OptimisticEvent) => void;
1529
1643
 
1530
1644
  /**
1531
1645
  * OptimisticManager - Handles optimistic UI updates with rollback
1532
- *
1533
- * Pattern:
1534
- * 1. User action (like) -> Update UI immediately
1535
- * 2. Call API in background
1536
- * 3. On success -> Keep UI state, remove pending action
1537
- * 4. On error -> Rollback to previous state
1538
- *
1539
- * Features:
1540
- * - Immediate UI feedback
1541
- * - Automatic rollback on failure
1542
- * - Retry queue for failed actions
1543
- * - Prevents duplicate pending actions
1544
- *
1545
- * @example
1546
- * ```typescript
1547
- * const optimistic = new OptimisticManager({
1548
- * interaction,
1549
- * feedManager,
1550
- * logger,
1551
- * });
1552
- *
1553
- * // User taps like button
1554
- * await optimistic.like(videoId);
1555
- *
1556
- * // User taps follow button
1557
- * await optimistic.follow(videoId);
1558
- * ```
1559
1646
  */
1560
1647
  declare class OptimisticManager {
1561
1648
  /** Zustand vanilla store - Single Source of Truth */
@@ -1564,7 +1651,7 @@ declare class OptimisticManager {
1564
1651
  private readonly config;
1565
1652
  /** Interaction adapter */
1566
1653
  private readonly interaction?;
1567
- /** Feed manager for updating video state */
1654
+ /** Feed manager for updating item state */
1568
1655
  private readonly feedManager?;
1569
1656
  /** Logger adapter */
1570
1657
  private readonly logger?;
@@ -1572,137 +1659,65 @@ declare class OptimisticManager {
1572
1659
  private readonly eventListeners;
1573
1660
  /** Retry timer */
1574
1661
  private retryTimer;
1575
- /** Debounce timers for like/unlike per video */
1662
+ /** Debounce timers for like/unlike per item */
1576
1663
  private readonly likeDebounceTimers;
1664
+ /** Intended like state while debouncing (itemId -> isLiked) */
1665
+ private readonly intendedLikeState;
1577
1666
  /** Debounce delay in ms */
1578
1667
  private readonly debounceDelay;
1579
1668
  constructor(config?: OptimisticConfig);
1580
1669
  /**
1581
- * Like a video with optimistic update
1670
+ * Like an item with optimistic update
1582
1671
  */
1583
- like(videoId: string): Promise<boolean>;
1672
+ like(itemId: string): Promise<boolean>;
1584
1673
  /**
1585
- * Unlike a video with optimistic update
1674
+ * Unlike an item with optimistic update
1586
1675
  */
1587
- unlike(videoId: string): Promise<boolean>;
1676
+ unlike(itemId: string): Promise<boolean>;
1588
1677
  /**
1589
- * Toggle like state with DEBOUNCE (like if not liked, unlike if liked)
1590
- *
1591
- * This method:
1592
- * 1. Updates UI immediately (optimistic)
1593
- * 2. Debounces API call - only sends after user stops clicking
1594
- * 3. Sends final state to API after debounce delay
1595
- *
1596
- * Perfect for rapid tapping like TikTok/Instagram behavior.
1678
+ * Toggle like state with DEBOUNCE
1597
1679
  */
1598
- toggleLike(videoId: string): void;
1680
+ toggleLike(itemId: string): void;
1599
1681
  /**
1600
- * Execute the actual API call after debounce
1601
- * Reads current state from FeedManager to get final intended state
1682
+ * Execute API call after debounce delay
1602
1683
  */
1603
1684
  private executeDebouncedLikeApi;
1604
1685
  /**
1605
- * @deprecated Use toggleLike() instead - it now includes debounce
1606
- * Legacy toggle that waits for API response
1686
+ * Handle errors from debounced API calls
1607
1687
  */
1608
- toggleLikeSync(videoId: string): Promise<boolean>;
1688
+ private handleDebouncedApiError;
1609
1689
  /**
1610
- * Follow a video author with optimistic update
1690
+ * Remove a pending action by ID
1611
1691
  */
1612
- follow(videoId: string): Promise<boolean>;
1613
- /**
1614
- * Unfollow a video author with optimistic update
1615
- */
1616
- unfollow(videoId: string): Promise<boolean>;
1692
+ private removePendingAction;
1617
1693
  /**
1618
1694
  * Toggle follow state
1619
1695
  */
1620
- 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;
1621
1701
  /**
1622
- * Get all pending actions
1702
+ * Reset all optimistic state
1623
1703
  */
1704
+ reset(): void;
1624
1705
  getPendingActions(): PendingAction[];
1625
- /**
1626
- * Check if there's a pending action for a video
1627
- * Only returns true for actions with status 'pending' (not 'failed')
1628
- */
1629
- hasPendingAction(videoId: string, type?: ActionType): boolean;
1630
- /**
1631
- * Get failed actions queue
1632
- */
1706
+ hasPendingAction(itemId: string, type?: ActionType): boolean;
1633
1707
  getFailedQueue(): PendingAction[];
1634
- /**
1635
- * Manually retry failed actions
1636
- */
1637
1708
  retryFailed(): Promise<void>;
1638
- /**
1639
- * Clear all failed actions
1640
- */
1641
1709
  clearFailed(): void;
1642
- /**
1643
- * Reset manager state
1644
- */
1645
- reset(): void;
1646
- /**
1647
- * Destroy manager and cleanup
1648
- */
1649
1710
  destroy(): void;
1650
- /**
1651
- * Add event listener
1652
- */
1653
- addEventListener(listener: OptimisticEventListener): () => void;
1654
- /**
1655
- * Remove event listener
1656
- */
1657
- removeEventListener(listener: OptimisticEventListener): void;
1658
- /**
1659
- * Perform an optimistic action
1660
- */
1711
+ private emit;
1661
1712
  private performAction;
1662
- /**
1663
- * Create rollback data based on action type
1664
- */
1665
1713
  private createRollbackData;
1666
- /**
1667
- * Apply optimistic update to feed
1668
- */
1669
1714
  private applyOptimisticUpdate;
1670
- /**
1671
- * Apply rollback
1672
- */
1673
1715
  private applyRollback;
1674
- /**
1675
- * Add pending action to store
1676
- */
1677
1716
  private addPendingAction;
1678
- /**
1679
- * Mark action as success
1680
- */
1681
1717
  private markActionSuccess;
1682
- /**
1683
- * Mark action as failed
1684
- */
1685
1718
  private markActionFailed;
1686
- /**
1687
- * Retry a failed action
1688
- */
1689
1719
  private retryAction;
1690
- /**
1691
- * Execute API call based on action type
1692
- */
1693
- private executeApiCall;
1694
- /**
1695
- * Schedule retry of failed actions
1696
- */
1697
1720
  private scheduleRetry;
1698
- /**
1699
- * Cancel retry timer
1700
- */
1701
- private cancelRetryTimer;
1702
- /**
1703
- * Emit event to all listeners
1704
- */
1705
- private emitEvent;
1706
1721
  }
1707
1722
 
1708
1723
  /**
@@ -1939,4 +1954,152 @@ declare class CommentManager {
1939
1954
  private createError;
1940
1955
  }
1941
1956
 
1942
- 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 };