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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { VideoItem, IDataSource, IAnalytics, ILogger, ISessionStorage, SessionSnapshot, INetworkAdapter, IVideoLoader, IPosterLoader, VideoSource, IInteraction } from '@xhub-short/contracts';
1
+ import { VideoItem, IDataSource, IStorage, PrefetchCacheData, IAnalytics, ILogger, ISessionStorage, SessionSnapshot, INetworkAdapter, IVideoLoader, IPosterLoader, VideoSource, IInteraction, CommentItem, ReplyItem, ICommentAdapter, InternalLogger, CommentAuthor } from '@xhub-short/contracts';
2
2
  export { SessionSnapshot } from '@xhub-short/contracts';
3
3
  import { StoreApi } from 'zustand/vanilla';
4
4
 
@@ -68,6 +68,58 @@ interface FeedConfig {
68
68
  * Default feed configuration
69
69
  */
70
70
  declare const DEFAULT_FEED_CONFIG: Required<FeedConfig>;
71
+ /**
72
+ * Configuration for prefetch cache feature
73
+ *
74
+ * Enables instant feed loading on fresh app open by caching
75
+ * the last N videos and showing them immediately while
76
+ * fresh data is fetched in the background.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const prefetchConfig: PrefetchCacheConfig = {
81
+ * enabled: true,
82
+ * maxVideos: 15,
83
+ * enableDynamicEviction: true,
84
+ * evictionThrottleMs: 2000,
85
+ * };
86
+ * ```
87
+ */
88
+ interface PrefetchCacheConfig {
89
+ /**
90
+ * Enable prefetch cache for instant loading
91
+ * @default true
92
+ */
93
+ enabled: boolean;
94
+ /**
95
+ * Maximum number of videos to cache
96
+ * Higher = more instant content, but more storage
97
+ * @default 10
98
+ */
99
+ maxVideos: number;
100
+ /**
101
+ * Storage key for prefetch cache
102
+ * @default 'sv-prefetch-cache'
103
+ */
104
+ storageKey: string;
105
+ /**
106
+ * Enable dynamic cache eviction
107
+ * When user scrolls past cached videos, remove them from cache
108
+ * Prevents user from rewatching same videos on reload
109
+ * @default true
110
+ */
111
+ enableDynamicEviction: boolean;
112
+ /**
113
+ * Throttle duration (ms) for dynamic eviction
114
+ * Reduces storage writes when user scrolls quickly
115
+ * @default 2000
116
+ */
117
+ evictionThrottleMs: number;
118
+ }
119
+ /**
120
+ * Default prefetch cache configuration
121
+ */
122
+ declare const DEFAULT_PREFETCH_CACHE_CONFIG: PrefetchCacheConfig;
71
123
 
72
124
  /**
73
125
  * FeedManager - Manages video feed data with zustand/vanilla store
@@ -101,6 +153,10 @@ declare class FeedManager {
101
153
  readonly store: StoreApi<FeedState>;
102
154
  /** Resolved configuration */
103
155
  private readonly config;
156
+ /** Prefetch cache configuration */
157
+ private readonly prefetchConfig;
158
+ /** Storage adapter for prefetch cache (optional) */
159
+ private readonly storage;
104
160
  /** Abort controller for cancelling in-flight requests */
105
161
  private abortController;
106
162
  /**
@@ -113,7 +169,26 @@ declare class FeedManager {
113
169
  * Used for garbage collection
114
170
  */
115
171
  private accessOrder;
116
- constructor(dataSource: IDataSource, config?: FeedConfig);
172
+ constructor(dataSource: IDataSource, config?: FeedConfig, storage?: IStorage, prefetchConfig?: Partial<PrefetchCacheConfig>);
173
+ /** Static memory cache for explicit prefetching */
174
+ private static globalMemoryCache;
175
+ /**
176
+ * Prefetch feed data and store in global memory cache
177
+ *
178
+ * @param dataSource - Data source to fetch from
179
+ * @param options - Prefetch options
180
+ */
181
+ static prefetch(dataSource: IDataSource, options?: {
182
+ ttl?: number;
183
+ }): Promise<void>;
184
+ /**
185
+ * Check if prefetch cache exists and is valid
186
+ */
187
+ static hasPrefetchCache(): boolean;
188
+ /**
189
+ * Clear prefetch cache
190
+ */
191
+ static clearPrefetchCache(): void;
117
192
  /**
118
193
  * Load initial feed data
119
194
  *
@@ -163,6 +238,26 @@ declare class FeedManager {
163
238
  * Update a video in the feed (for optimistic updates)
164
239
  */
165
240
  updateVideo(id: string, updates: Partial<VideoItem>): void;
241
+ /**
242
+ * Remove an item from the feed
243
+ *
244
+ * Used for:
245
+ * - Report: Remove reported content from feed
246
+ * - Not Interested: Remove content user doesn't want to see
247
+ *
248
+ * @param id - Content ID to remove
249
+ * @returns true if item was removed, false if not found
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * // User reports a video
254
+ * const wasRemoved = feedManager.removeItem(videoId);
255
+ * if (wasRemoved) {
256
+ * // Navigate to next video
257
+ * }
258
+ * ```
259
+ */
260
+ removeItem(id: string): boolean;
166
261
  /**
167
262
  * Check if data is stale and needs revalidation
168
263
  */
@@ -193,6 +288,47 @@ declare class FeedManager {
193
288
  /** Whether to mark data as stale for background revalidation */
194
289
  markAsStale?: boolean;
195
290
  }): void;
291
+ /**
292
+ * Update prefetch cache with current feed tail
293
+ * Called automatically after loadInitial() and loadMore()
294
+ *
295
+ * Strategy: Cache the LAST N videos (tail of feed)
296
+ * These are videos user hasn't seen yet, perfect for instant display
297
+ */
298
+ updatePrefetchCache(): void;
299
+ /**
300
+ * Save prefetch cache to storage (async, non-blocking)
301
+ */
302
+ private savePrefetchCacheAsync;
303
+ /**
304
+ * Load prefetch cache from storage
305
+ * Returns null if no cache, disabled, or storage error
306
+ */
307
+ loadPrefetchCache(): Promise<PrefetchCacheData | null>;
308
+ /**
309
+ * Hydrate feed from prefetch cache for instant display
310
+ * Marks data as stale to trigger background revalidation
311
+ */
312
+ hydrateFromPrefetchCache(cache: PrefetchCacheData): void;
313
+ /**
314
+ * Clear prefetch cache
315
+ * Call when user logs out or data should be invalidated
316
+ */
317
+ clearPrefetchCache(): Promise<void>;
318
+ /**
319
+ * Evict videos that user has scrolled past from prefetch cache
320
+ * Called when user's focusedIndex changes
321
+ *
322
+ * Strategy: Remove all videos at or before current position
323
+ * This ensures user doesn't rewatch videos on reload
324
+ *
325
+ * @param currentIndex - Current focused video index in feed
326
+ */
327
+ evictViewedVideosFromCache(currentIndex: number): Promise<void>;
328
+ /**
329
+ * Get prefetch cache configuration (for external access)
330
+ */
331
+ getPrefetchConfig(): PrefetchCacheConfig;
196
332
  /**
197
333
  * Fetch with exponential backoff retry
198
334
  */
@@ -326,6 +462,13 @@ interface PlayerState {
326
462
  * Only the video with this ID should use `pendingRestoreTime`.
327
463
  */
328
464
  pendingRestoreVideoId: string | null;
465
+ /**
466
+ * Captured video frame at restore position (base64 JPEG).
467
+ *
468
+ * Displayed as instant preview while video loads.
469
+ * Cleared after being consumed by VideoPlayer/VideoSlot.
470
+ */
471
+ pendingRestoreFrame: string | null;
329
472
  }
330
473
  /**
331
474
  * PlayerEngine configuration
@@ -582,21 +725,40 @@ declare class PlayerEngine {
582
725
  * ```typescript
583
726
  * // During session restore
584
727
  * if (result.playbackTime && result.currentVideoId) {
585
- * playerEngine.setRestorePosition(result.currentVideoId, result.playbackTime);
728
+ * playerEngine.setRestorePosition(result.currentVideoId, result.playbackTime, result.restoreFrame);
586
729
  * }
587
730
  * ```
588
731
  */
589
- setRestorePosition(videoId: string, time: number): void;
732
+ setRestorePosition(videoId: string, time: number, frame?: string): void;
590
733
  /**
591
734
  * Get and clear restore position for a video.
592
735
  *
593
736
  * VideoPlayer calls this when rendering to get startTime.
594
737
  * Position is cleared after being read to prevent reuse.
738
+ * Note: restoreFrame is NOT cleared here - use consumeRestoreFrame() separately.
595
739
  *
596
740
  * @param videoId - Video ID to check
597
741
  * @returns Restore time in seconds, or null if no pending restore
598
742
  */
599
743
  consumeRestorePosition(videoId: string): number | null;
744
+ /**
745
+ * Get and clear restore frame for a video.
746
+ *
747
+ * VideoSlot calls this to display instant preview while video loads.
748
+ * Frame is cleared after being read to free memory.
749
+ *
750
+ * @param videoId - Video ID to check
751
+ * @returns Base64 JPEG data URL, or null if no pending frame
752
+ */
753
+ consumeRestoreFrame(videoId: string): string | null;
754
+ /**
755
+ * Get restore frame without consuming (peek).
756
+ * Used to display preview while keeping it available.
757
+ *
758
+ * @param videoId - Video ID to check
759
+ * @returns Base64 JPEG data URL, or null if no pending frame
760
+ */
761
+ peekRestoreFrame(videoId: string): string | null;
600
762
  /**
601
763
  * Check if there's a pending restore position for a video.
602
764
  *
@@ -646,6 +808,7 @@ declare class PlayerEngine {
646
808
  private emitEvent;
647
809
  /**
648
810
  * Start watch time tracking
811
+ * Increments watchTime every second and sends analytics heartbeat
649
812
  */
650
813
  private startWatchTimeTracking;
651
814
  /**
@@ -653,9 +816,14 @@ declare class PlayerEngine {
653
816
  */
654
817
  private stopWatchTimeTracking;
655
818
  /**
656
- * Track completion analytics
819
+ * Track completion analytics (when video loops/ends)
657
820
  */
658
821
  private trackCompletion;
822
+ /**
823
+ * Track when user leaves current video (scrolls to next video)
824
+ * Sends final analytics event before video change
825
+ */
826
+ private trackLeaveVideo;
659
827
  /**
660
828
  * Categorize media error
661
829
  */
@@ -764,6 +932,8 @@ interface RestoreResult {
764
932
  playbackTime?: number;
765
933
  /** Video ID that was playing (only present if restorePlaybackPosition config is enabled) */
766
934
  currentVideoId?: string;
935
+ /** Captured video frame at playback position (base64 JPEG, only present if restorePlaybackPosition is enabled) */
936
+ restoreFrame?: string;
767
937
  }
768
938
  /**
769
939
  * LifecycleManager configuration
@@ -886,6 +1056,7 @@ declare class LifecycleManager {
886
1056
  * @param data - Snapshot data to save
887
1057
  * @param data.playbackTime - Current video playback position (only saved if restorePlaybackPosition config is enabled)
888
1058
  * @param data.currentVideoId - Current video ID (only saved if restorePlaybackPosition config is enabled)
1059
+ * @param data.restoreFrame - Captured video frame at playback position (only saved if restorePlaybackPosition is enabled)
889
1060
  */
890
1061
  saveSnapshot(data: {
891
1062
  items: VideoItem[];
@@ -896,6 +1067,8 @@ declare class LifecycleManager {
896
1067
  playbackTime?: number;
897
1068
  /** Current video ID (only used when restorePlaybackPosition is enabled) */
898
1069
  currentVideoId?: string;
1070
+ /** Captured video frame at playback position (only used when restorePlaybackPosition is enabled) */
1071
+ restoreFrame?: string;
899
1072
  }): Promise<boolean>;
900
1073
  /**
901
1074
  * Clear saved snapshot
@@ -1552,4 +1725,238 @@ declare class OptimisticManager {
1552
1725
  private emitEvent;
1553
1726
  }
1554
1727
 
1555
- export { type ActionType, type AllocationResult, DEFAULT_FEED_CONFIG, DEFAULT_LIFECYCLE_CONFIG, DEFAULT_OPTIMISTIC_CONFIG, DEFAULT_PLAYER_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 OptimisticConfig, type OptimisticEvent, type OptimisticEventListener, OptimisticManager, type OptimisticState, type PendingAction, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PrefetchConfig, type ResourceConfig, type ResourceEvent, type ResourceEventListener, ResourceGovernor, type ResourceState, type RestoreResult, calculatePrefetchIndices, calculateWindowIndices, canPause, canPlay, canSeek, computeAllocationChanges, isActiveState, isValidTransition, mapNetworkType };
1728
+ /**
1729
+ * Comment state for a single video in zustand store
1730
+ */
1731
+ interface VideoCommentState {
1732
+ /** Normalized comments - Map for O(1) lookup */
1733
+ commentsById: Map<string, CommentItem>;
1734
+ /** Ordered list of comment IDs for rendering */
1735
+ displayOrder: string[];
1736
+ /** Total comment count from API */
1737
+ totalCount: number;
1738
+ /** Cursor for pagination */
1739
+ cursor: string | null;
1740
+ /** Whether more comments can be loaded */
1741
+ hasMore: boolean;
1742
+ /** Initial loading state */
1743
+ loading: boolean;
1744
+ /** Loading more (pagination) state */
1745
+ loadingMore: boolean;
1746
+ /** Error state */
1747
+ error: CommentError | null;
1748
+ /** Cache timestamp for TTL */
1749
+ cachedAt: number;
1750
+ /** Whether data is stale */
1751
+ isStale: boolean;
1752
+ }
1753
+ /**
1754
+ * Global comment manager state
1755
+ */
1756
+ interface CommentManagerState {
1757
+ /** Comments by video ID */
1758
+ byVideoId: Map<string, VideoCommentState>;
1759
+ /** Currently active video ID (for comment sheet) */
1760
+ activeVideoId: string | null;
1761
+ /** Posting state */
1762
+ isPosting: boolean;
1763
+ /** Posting error */
1764
+ postError: CommentError | null;
1765
+ /** Deleting comment ID (for optimistic UI) */
1766
+ deletingId: string | null;
1767
+ }
1768
+ /**
1769
+ * Comment error with additional context
1770
+ */
1771
+ interface CommentError {
1772
+ /** Error message */
1773
+ message: string;
1774
+ /** Error code for programmatic handling */
1775
+ code: 'NETWORK_ERROR' | 'TIMEOUT' | 'SERVER_ERROR' | 'NOT_FOUND' | 'FORBIDDEN' | 'RATE_LIMITED' | 'UNKNOWN';
1776
+ /** Number of retry attempts made */
1777
+ retryCount: number;
1778
+ /** Whether error is recoverable */
1779
+ recoverable: boolean;
1780
+ }
1781
+ /**
1782
+ * CommentManager configuration
1783
+ */
1784
+ interface CommentManagerConfig {
1785
+ /** Comments per page (default: 20) */
1786
+ pageSize?: number;
1787
+ /** Replies per load (default: 10) */
1788
+ repliesPageSize?: number;
1789
+ /** Cache TTL in ms (default: 5 minutes) */
1790
+ cacheTTL?: number;
1791
+ /** Maximum retry attempts (default: 2) */
1792
+ maxRetries?: number;
1793
+ /** Auto-expand threshold for replies (default: 1) */
1794
+ repliesAutoExpandThreshold?: number;
1795
+ /** Replies collapse limit (default: 3) */
1796
+ repliesCollapseLimit?: number;
1797
+ /** Comment text max lines before collapse (default: 3) */
1798
+ textMaxLines?: number;
1799
+ /** Enable optimistic UI (default: true) */
1800
+ enableOptimistic?: boolean;
1801
+ }
1802
+ /**
1803
+ * Default comment manager configuration
1804
+ */
1805
+ declare const DEFAULT_COMMENT_MANAGER_CONFIG: Required<CommentManagerConfig>;
1806
+ /**
1807
+ * Create initial state for a video's comments
1808
+ */
1809
+ declare const createInitialVideoCommentState: () => VideoCommentState;
1810
+ /**
1811
+ * Create initial global comment state
1812
+ */
1813
+ declare const createInitialCommentState: () => CommentManagerState;
1814
+ /**
1815
+ * Optimistic comment for immediate UI feedback
1816
+ */
1817
+ interface OptimisticComment extends Omit<CommentItem, 'id'> {
1818
+ /** Temporary optimistic ID (prefixed with 'optimistic_') */
1819
+ id: string;
1820
+ /** Flag to identify optimistic items */
1821
+ isPending: true;
1822
+ }
1823
+ /**
1824
+ * Optimistic reply for immediate UI feedback
1825
+ */
1826
+ interface OptimisticReply extends Omit<ReplyItem, 'id'> {
1827
+ /** Temporary optimistic ID */
1828
+ id: string;
1829
+ /** Flag to identify optimistic items */
1830
+ isPending: true;
1831
+ }
1832
+
1833
+ /**
1834
+ * CommentManager - Manages comment data with zustand/vanilla store
1835
+ *
1836
+ * Features:
1837
+ * - Multi-video comment caching (by videoId)
1838
+ * - Pagination with cursor
1839
+ * - Nested replies support (1 level)
1840
+ * - Optimistic UI for post/delete
1841
+ * - Cache TTL for stale data detection
1842
+ * - Request deduplication
1843
+ *
1844
+ * Architecture: Hexagonal (Port = ICommentAdapter)
1845
+ *
1846
+ * @example
1847
+ * ```typescript
1848
+ * const commentManager = new CommentManager(commentAdapter);
1849
+ *
1850
+ * // Subscribe to state changes
1851
+ * commentManager.store.subscribe((state) => console.log(state));
1852
+ *
1853
+ * // Load comments for a video
1854
+ * await commentManager.loadComments('video-123');
1855
+ *
1856
+ * // Post a comment
1857
+ * await commentManager.postComment('video-123', 'Great video!');
1858
+ * ```
1859
+ */
1860
+ declare class CommentManager {
1861
+ private readonly adapter;
1862
+ /** Zustand vanilla store - Single Source of Truth */
1863
+ readonly store: StoreApi<CommentManagerState>;
1864
+ /** Resolved configuration */
1865
+ private readonly config;
1866
+ /** Logger (optional) */
1867
+ private readonly logger?;
1868
+ /** Request deduplication: Map of key → in-flight Promise */
1869
+ private inFlightRequests;
1870
+ /** Optimistic ID counter */
1871
+ private optimisticIdCounter;
1872
+ constructor(adapter: ICommentAdapter, config?: CommentManagerConfig, logger?: InternalLogger);
1873
+ /**
1874
+ * Load comments for a video
1875
+ *
1876
+ * Features:
1877
+ * - Cache check with TTL
1878
+ * - Request deduplication
1879
+ * - Stale data detection
1880
+ */
1881
+ loadComments(videoId: string, forceRefresh?: boolean): Promise<void>;
1882
+ /**
1883
+ * Load more comments (pagination)
1884
+ */
1885
+ loadMore(videoId: string): Promise<void>;
1886
+ /**
1887
+ * Load replies for a comment
1888
+ */
1889
+ loadReplies(commentId: string): Promise<void>;
1890
+ /**
1891
+ * Post a new comment with optimistic UI
1892
+ */
1893
+ postComment(videoId: string, content: string, currentUser: CommentAuthor): Promise<CommentItem | null>;
1894
+ /**
1895
+ * Post a reply with optimistic UI
1896
+ */
1897
+ postReply(videoId: string, parentId: string, content: string, currentUser: CommentAuthor, replyTo?: CommentAuthor): Promise<ReplyItem | null>;
1898
+ /**
1899
+ * Delete a comment with optimistic UI
1900
+ */
1901
+ deleteComment(videoId: string, commentId: string, isReply?: boolean, parentId?: string): Promise<boolean>;
1902
+ /**
1903
+ * Like a comment/reply with optimistic UI
1904
+ */
1905
+ likeComment(videoId: string, commentId: string, isReply?: boolean, parentId?: string): Promise<void>;
1906
+ /**
1907
+ * Unlike a comment/reply with optimistic UI
1908
+ */
1909
+ unlikeComment(videoId: string, commentId: string, isReply?: boolean, parentId?: string): Promise<void>;
1910
+ /**
1911
+ * Get comments for a video
1912
+ */
1913
+ getComments(videoId: string): CommentItem[];
1914
+ /**
1915
+ * Get a single comment
1916
+ */
1917
+ getComment(videoId: string, commentId: string): CommentItem | undefined;
1918
+ /**
1919
+ * Get a single reply
1920
+ */
1921
+ getReply(videoId: string, parentId: string, replyId: string): ReplyItem | undefined;
1922
+ /**
1923
+ * Get video comment state
1924
+ */
1925
+ getVideoState(videoId: string): VideoCommentState | undefined;
1926
+ /**
1927
+ * Set active video ID (for comment sheet)
1928
+ */
1929
+ setActiveVideo(videoId: string | null): void;
1930
+ /**
1931
+ * Get config
1932
+ */
1933
+ getConfig(): Required<CommentManagerConfig>;
1934
+ /**
1935
+ * Clear comments for a video
1936
+ */
1937
+ clearVideoComments(videoId: string): void;
1938
+ /**
1939
+ * Clear all comments
1940
+ */
1941
+ clearAll(): void;
1942
+ private executeLoadComments;
1943
+ private executeLoadMore;
1944
+ private executeLoadReplies;
1945
+ private addOptimisticComment;
1946
+ private removeOptimisticComment;
1947
+ private replaceOptimisticComment;
1948
+ private addOptimisticReply;
1949
+ private removeOptimisticReply;
1950
+ private replaceOptimisticReply;
1951
+ private removeCommentFromStore;
1952
+ private removeReplyFromStore;
1953
+ private restoreComment;
1954
+ private restoreReply;
1955
+ private updateLikeState;
1956
+ private updateVideoState;
1957
+ private isCacheStale;
1958
+ private generateOptimisticId;
1959
+ private createError;
1960
+ }
1961
+
1962
+ 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 };