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

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,7 @@ 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>);
117
173
  /**
118
174
  * Load initial feed data
119
175
  *
@@ -193,6 +249,47 @@ declare class FeedManager {
193
249
  /** Whether to mark data as stale for background revalidation */
194
250
  markAsStale?: boolean;
195
251
  }): void;
252
+ /**
253
+ * Update prefetch cache with current feed tail
254
+ * Called automatically after loadInitial() and loadMore()
255
+ *
256
+ * Strategy: Cache the LAST N videos (tail of feed)
257
+ * These are videos user hasn't seen yet, perfect for instant display
258
+ */
259
+ updatePrefetchCache(): void;
260
+ /**
261
+ * Save prefetch cache to storage (async, non-blocking)
262
+ */
263
+ private savePrefetchCacheAsync;
264
+ /**
265
+ * Load prefetch cache from storage
266
+ * Returns null if no cache, disabled, or storage error
267
+ */
268
+ loadPrefetchCache(): Promise<PrefetchCacheData | null>;
269
+ /**
270
+ * Hydrate feed from prefetch cache for instant display
271
+ * Marks data as stale to trigger background revalidation
272
+ */
273
+ hydrateFromPrefetchCache(cache: PrefetchCacheData): void;
274
+ /**
275
+ * Clear prefetch cache
276
+ * Call when user logs out or data should be invalidated
277
+ */
278
+ clearPrefetchCache(): Promise<void>;
279
+ /**
280
+ * Evict videos that user has scrolled past from prefetch cache
281
+ * Called when user's focusedIndex changes
282
+ *
283
+ * Strategy: Remove all videos at or before current position
284
+ * This ensures user doesn't rewatch videos on reload
285
+ *
286
+ * @param currentIndex - Current focused video index in feed
287
+ */
288
+ evictViewedVideosFromCache(currentIndex: number): Promise<void>;
289
+ /**
290
+ * Get prefetch cache configuration (for external access)
291
+ */
292
+ getPrefetchConfig(): PrefetchCacheConfig;
196
293
  /**
197
294
  * Fetch with exponential backoff retry
198
295
  */
@@ -326,6 +423,13 @@ interface PlayerState {
326
423
  * Only the video with this ID should use `pendingRestoreTime`.
327
424
  */
328
425
  pendingRestoreVideoId: string | null;
426
+ /**
427
+ * Captured video frame at restore position (base64 JPEG).
428
+ *
429
+ * Displayed as instant preview while video loads.
430
+ * Cleared after being consumed by VideoPlayer/VideoSlot.
431
+ */
432
+ pendingRestoreFrame: string | null;
329
433
  }
330
434
  /**
331
435
  * PlayerEngine configuration
@@ -582,21 +686,40 @@ declare class PlayerEngine {
582
686
  * ```typescript
583
687
  * // During session restore
584
688
  * if (result.playbackTime && result.currentVideoId) {
585
- * playerEngine.setRestorePosition(result.currentVideoId, result.playbackTime);
689
+ * playerEngine.setRestorePosition(result.currentVideoId, result.playbackTime, result.restoreFrame);
586
690
  * }
587
691
  * ```
588
692
  */
589
- setRestorePosition(videoId: string, time: number): void;
693
+ setRestorePosition(videoId: string, time: number, frame?: string): void;
590
694
  /**
591
695
  * Get and clear restore position for a video.
592
696
  *
593
697
  * VideoPlayer calls this when rendering to get startTime.
594
698
  * Position is cleared after being read to prevent reuse.
699
+ * Note: restoreFrame is NOT cleared here - use consumeRestoreFrame() separately.
595
700
  *
596
701
  * @param videoId - Video ID to check
597
702
  * @returns Restore time in seconds, or null if no pending restore
598
703
  */
599
704
  consumeRestorePosition(videoId: string): number | null;
705
+ /**
706
+ * Get and clear restore frame for a video.
707
+ *
708
+ * VideoSlot calls this to display instant preview while video loads.
709
+ * Frame is cleared after being read to free memory.
710
+ *
711
+ * @param videoId - Video ID to check
712
+ * @returns Base64 JPEG data URL, or null if no pending frame
713
+ */
714
+ consumeRestoreFrame(videoId: string): string | null;
715
+ /**
716
+ * Get restore frame without consuming (peek).
717
+ * Used to display preview while keeping it available.
718
+ *
719
+ * @param videoId - Video ID to check
720
+ * @returns Base64 JPEG data URL, or null if no pending frame
721
+ */
722
+ peekRestoreFrame(videoId: string): string | null;
600
723
  /**
601
724
  * Check if there's a pending restore position for a video.
602
725
  *
@@ -764,6 +887,8 @@ interface RestoreResult {
764
887
  playbackTime?: number;
765
888
  /** Video ID that was playing (only present if restorePlaybackPosition config is enabled) */
766
889
  currentVideoId?: string;
890
+ /** Captured video frame at playback position (base64 JPEG, only present if restorePlaybackPosition is enabled) */
891
+ restoreFrame?: string;
767
892
  }
768
893
  /**
769
894
  * LifecycleManager configuration
@@ -886,6 +1011,7 @@ declare class LifecycleManager {
886
1011
  * @param data - Snapshot data to save
887
1012
  * @param data.playbackTime - Current video playback position (only saved if restorePlaybackPosition config is enabled)
888
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)
889
1015
  */
890
1016
  saveSnapshot(data: {
891
1017
  items: VideoItem[];
@@ -896,6 +1022,8 @@ declare class LifecycleManager {
896
1022
  playbackTime?: number;
897
1023
  /** Current video ID (only used when restorePlaybackPosition is enabled) */
898
1024
  currentVideoId?: string;
1025
+ /** Captured video frame at playback position (only used when restorePlaybackPosition is enabled) */
1026
+ restoreFrame?: string;
899
1027
  }): Promise<boolean>;
900
1028
  /**
901
1029
  * Clear saved snapshot
@@ -1552,4 +1680,238 @@ declare class OptimisticManager {
1552
1680
  private emitEvent;
1553
1681
  }
1554
1682
 
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 };
1683
+ /**
1684
+ * Comment state for a single video in zustand store
1685
+ */
1686
+ interface VideoCommentState {
1687
+ /** Normalized comments - Map for O(1) lookup */
1688
+ commentsById: Map<string, CommentItem>;
1689
+ /** Ordered list of comment IDs for rendering */
1690
+ displayOrder: string[];
1691
+ /** Total comment count from API */
1692
+ totalCount: number;
1693
+ /** Cursor for pagination */
1694
+ cursor: string | null;
1695
+ /** Whether more comments can be loaded */
1696
+ hasMore: boolean;
1697
+ /** Initial loading state */
1698
+ loading: boolean;
1699
+ /** Loading more (pagination) state */
1700
+ loadingMore: boolean;
1701
+ /** Error state */
1702
+ error: CommentError | null;
1703
+ /** Cache timestamp for TTL */
1704
+ cachedAt: number;
1705
+ /** Whether data is stale */
1706
+ isStale: boolean;
1707
+ }
1708
+ /**
1709
+ * Global comment manager state
1710
+ */
1711
+ interface CommentManagerState {
1712
+ /** Comments by video ID */
1713
+ byVideoId: Map<string, VideoCommentState>;
1714
+ /** Currently active video ID (for comment sheet) */
1715
+ activeVideoId: string | null;
1716
+ /** Posting state */
1717
+ isPosting: boolean;
1718
+ /** Posting error */
1719
+ postError: CommentError | null;
1720
+ /** Deleting comment ID (for optimistic UI) */
1721
+ deletingId: string | null;
1722
+ }
1723
+ /**
1724
+ * Comment error with additional context
1725
+ */
1726
+ interface CommentError {
1727
+ /** Error message */
1728
+ message: string;
1729
+ /** Error code for programmatic handling */
1730
+ code: 'NETWORK_ERROR' | 'TIMEOUT' | 'SERVER_ERROR' | 'NOT_FOUND' | 'FORBIDDEN' | 'RATE_LIMITED' | 'UNKNOWN';
1731
+ /** Number of retry attempts made */
1732
+ retryCount: number;
1733
+ /** Whether error is recoverable */
1734
+ recoverable: boolean;
1735
+ }
1736
+ /**
1737
+ * CommentManager configuration
1738
+ */
1739
+ interface CommentManagerConfig {
1740
+ /** Comments per page (default: 20) */
1741
+ pageSize?: number;
1742
+ /** Replies per load (default: 10) */
1743
+ repliesPageSize?: number;
1744
+ /** Cache TTL in ms (default: 5 minutes) */
1745
+ cacheTTL?: number;
1746
+ /** Maximum retry attempts (default: 2) */
1747
+ maxRetries?: number;
1748
+ /** Auto-expand threshold for replies (default: 1) */
1749
+ repliesAutoExpandThreshold?: number;
1750
+ /** Replies collapse limit (default: 3) */
1751
+ repliesCollapseLimit?: number;
1752
+ /** Comment text max lines before collapse (default: 3) */
1753
+ textMaxLines?: number;
1754
+ /** Enable optimistic UI (default: true) */
1755
+ enableOptimistic?: boolean;
1756
+ }
1757
+ /**
1758
+ * Default comment manager configuration
1759
+ */
1760
+ declare const DEFAULT_COMMENT_MANAGER_CONFIG: Required<CommentManagerConfig>;
1761
+ /**
1762
+ * Create initial state for a video's comments
1763
+ */
1764
+ declare const createInitialVideoCommentState: () => VideoCommentState;
1765
+ /**
1766
+ * Create initial global comment state
1767
+ */
1768
+ declare const createInitialCommentState: () => CommentManagerState;
1769
+ /**
1770
+ * Optimistic comment for immediate UI feedback
1771
+ */
1772
+ interface OptimisticComment extends Omit<CommentItem, 'id'> {
1773
+ /** Temporary optimistic ID (prefixed with 'optimistic_') */
1774
+ id: string;
1775
+ /** Flag to identify optimistic items */
1776
+ isPending: true;
1777
+ }
1778
+ /**
1779
+ * Optimistic reply for immediate UI feedback
1780
+ */
1781
+ interface OptimisticReply extends Omit<ReplyItem, 'id'> {
1782
+ /** Temporary optimistic ID */
1783
+ id: string;
1784
+ /** Flag to identify optimistic items */
1785
+ isPending: true;
1786
+ }
1787
+
1788
+ /**
1789
+ * CommentManager - Manages comment data with zustand/vanilla store
1790
+ *
1791
+ * Features:
1792
+ * - Multi-video comment caching (by videoId)
1793
+ * - Pagination with cursor
1794
+ * - Nested replies support (1 level)
1795
+ * - Optimistic UI for post/delete
1796
+ * - Cache TTL for stale data detection
1797
+ * - Request deduplication
1798
+ *
1799
+ * Architecture: Hexagonal (Port = ICommentAdapter)
1800
+ *
1801
+ * @example
1802
+ * ```typescript
1803
+ * const commentManager = new CommentManager(commentAdapter);
1804
+ *
1805
+ * // Subscribe to state changes
1806
+ * commentManager.store.subscribe((state) => console.log(state));
1807
+ *
1808
+ * // Load comments for a video
1809
+ * await commentManager.loadComments('video-123');
1810
+ *
1811
+ * // Post a comment
1812
+ * await commentManager.postComment('video-123', 'Great video!');
1813
+ * ```
1814
+ */
1815
+ declare class CommentManager {
1816
+ private readonly adapter;
1817
+ /** Zustand vanilla store - Single Source of Truth */
1818
+ readonly store: StoreApi<CommentManagerState>;
1819
+ /** Resolved configuration */
1820
+ private readonly config;
1821
+ /** Logger (optional) */
1822
+ private readonly logger?;
1823
+ /** Request deduplication: Map of key → in-flight Promise */
1824
+ private inFlightRequests;
1825
+ /** Optimistic ID counter */
1826
+ private optimisticIdCounter;
1827
+ constructor(adapter: ICommentAdapter, config?: CommentManagerConfig, logger?: InternalLogger);
1828
+ /**
1829
+ * Load comments for a video
1830
+ *
1831
+ * Features:
1832
+ * - Cache check with TTL
1833
+ * - Request deduplication
1834
+ * - Stale data detection
1835
+ */
1836
+ loadComments(videoId: string, forceRefresh?: boolean): Promise<void>;
1837
+ /**
1838
+ * Load more comments (pagination)
1839
+ */
1840
+ loadMore(videoId: string): Promise<void>;
1841
+ /**
1842
+ * Load replies for a comment
1843
+ */
1844
+ loadReplies(commentId: string): Promise<void>;
1845
+ /**
1846
+ * Post a new comment with optimistic UI
1847
+ */
1848
+ postComment(videoId: string, content: string, currentUser: CommentAuthor): Promise<CommentItem | null>;
1849
+ /**
1850
+ * Post a reply with optimistic UI
1851
+ */
1852
+ postReply(videoId: string, parentId: string, content: string, currentUser: CommentAuthor, replyTo?: CommentAuthor): Promise<ReplyItem | null>;
1853
+ /**
1854
+ * Delete a comment with optimistic UI
1855
+ */
1856
+ deleteComment(videoId: string, commentId: string, isReply?: boolean, parentId?: string): Promise<boolean>;
1857
+ /**
1858
+ * Like a comment/reply with optimistic UI
1859
+ */
1860
+ likeComment(videoId: string, commentId: string, isReply?: boolean, parentId?: string): Promise<void>;
1861
+ /**
1862
+ * Unlike a comment/reply with optimistic UI
1863
+ */
1864
+ unlikeComment(videoId: string, commentId: string, isReply?: boolean, parentId?: string): Promise<void>;
1865
+ /**
1866
+ * Get comments for a video
1867
+ */
1868
+ getComments(videoId: string): CommentItem[];
1869
+ /**
1870
+ * Get a single comment
1871
+ */
1872
+ getComment(videoId: string, commentId: string): CommentItem | undefined;
1873
+ /**
1874
+ * Get a single reply
1875
+ */
1876
+ getReply(videoId: string, parentId: string, replyId: string): ReplyItem | undefined;
1877
+ /**
1878
+ * Get video comment state
1879
+ */
1880
+ getVideoState(videoId: string): VideoCommentState | undefined;
1881
+ /**
1882
+ * Set active video ID (for comment sheet)
1883
+ */
1884
+ setActiveVideo(videoId: string | null): void;
1885
+ /**
1886
+ * Get config
1887
+ */
1888
+ getConfig(): Required<CommentManagerConfig>;
1889
+ /**
1890
+ * Clear comments for a video
1891
+ */
1892
+ clearVideoComments(videoId: string): void;
1893
+ /**
1894
+ * Clear all comments
1895
+ */
1896
+ clearAll(): void;
1897
+ private executeLoadComments;
1898
+ private executeLoadMore;
1899
+ private executeLoadReplies;
1900
+ private addOptimisticComment;
1901
+ private removeOptimisticComment;
1902
+ private replaceOptimisticComment;
1903
+ private addOptimisticReply;
1904
+ private removeOptimisticReply;
1905
+ private replaceOptimisticReply;
1906
+ private removeCommentFromStore;
1907
+ private removeReplyFromStore;
1908
+ private restoreComment;
1909
+ private restoreReply;
1910
+ private updateLikeState;
1911
+ private updateVideoState;
1912
+ private isCacheStale;
1913
+ private generateOptimisticId;
1914
+ private createError;
1915
+ }
1916
+
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 };