@xhub-short/core 0.1.0-beta.16 → 0.1.0-beta.17

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 +69 -58
  2. package/dist/index.js +158 -141
  3. package/package.json +4 -4
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { INetworkAdapter, IVideoLoader, IPosterLoader, ILogger, VideoSource, VideoItem, IDataSource, IStorage, PrefetchCacheData, IAnalytics, ISessionStorage, SessionSnapshot, IInteraction, CommentItem, ReplyItem, ICommentAdapter, InternalLogger, CommentAuthor, PlaylistData, PlaylistSummary, ContentItem, IPlaylistDataSource } 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
 
@@ -313,9 +313,9 @@ declare class ResourceGovernor {
313
313
  * Feed state for zustand store
314
314
  */
315
315
  interface FeedState {
316
- /** Normalized video data - Map for O(1) lookup */
317
- itemsById: Map<string, VideoItem>;
318
- /** 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 */
319
319
  displayOrder: string[];
320
320
  /** Initial loading state */
321
321
  loading: boolean;
@@ -360,8 +360,8 @@ interface FeedConfig {
360
360
  /** Whether to enable SWR pattern (default: true) */
361
361
  enableSWR?: boolean;
362
362
  /**
363
- * Maximum number of videos to keep in memory cache
364
- * 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)
365
365
  * @default 100
366
366
  */
367
367
  maxCacheSize?: number;
@@ -399,11 +399,14 @@ interface PrefetchCacheConfig {
399
399
  */
400
400
  enabled: boolean;
401
401
  /**
402
- * Maximum number of videos to cache
402
+ * Maximum number of items to cache
403
403
  * Higher = more instant content, but more storage
404
- * @default 10
405
404
  */
406
- maxVideos: number;
405
+ maxItems: number;
406
+ /**
407
+ * @deprecated Use maxItems instead. Will be removed in v3.0
408
+ */
409
+ maxVideos?: number;
407
410
  /**
408
411
  * Storage key for prefetch cache
409
412
  * @default 'sv-prefetch-cache'
@@ -411,8 +414,8 @@ interface PrefetchCacheConfig {
411
414
  storageKey: string;
412
415
  /**
413
416
  * Enable dynamic cache eviction
414
- * When user scrolls past cached videos, remove them from cache
415
- * 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
416
419
  * @default true
417
420
  */
418
421
  enableDynamicEviction: boolean;
@@ -429,7 +432,7 @@ interface PrefetchCacheConfig {
429
432
  declare const DEFAULT_PREFETCH_CACHE_CONFIG: PrefetchCacheConfig;
430
433
 
431
434
  /**
432
- * FeedManager - Manages video feed data with zustand/vanilla store
435
+ * FeedManager - Manages content feed data with zustand/vanilla store
433
436
  *
434
437
  * Features:
435
438
  * - Data normalization (Map for O(1) lookup + ordered IDs)
@@ -478,10 +481,10 @@ declare class FeedManager {
478
481
  */
479
482
  private accessOrder;
480
483
  /**
481
- * Track videos that have already triggered predictive preload
484
+ * Track items that have already triggered predictive preload
482
485
  * to avoid duplicate requests.
483
486
  */
484
- private preloadedVideoIds;
487
+ private preloadedItemIds;
485
488
  /**
486
489
  * Recommend feed snapshot — preserved when switching to playlist mode.
487
490
  * Stored here (not in React refs) because FeedManager is a singleton
@@ -564,29 +567,37 @@ declare class FeedManager {
564
567
  /**
565
568
  * Handle playback progress and trigger predictive preloading
566
569
  *
567
- * @param videoId - ID of the currently playing video
570
+ * @param itemId - ID of the currently playing item
568
571
  * @param progress - Current playback progress (0-1)
569
572
  * @param governor - Resource governor to trigger preload
570
573
  * @param threshold - Progress threshold to trigger preload (default: 0.2)
571
574
  */
572
- handlePlaybackProgress(videoId: string, progress: number, governor: ResourceGovernor, threshold?: number): void;
575
+ handlePlaybackProgress(itemId: string, progress: number, governor: ResourceGovernor, threshold?: number): void;
576
+ getItem(id: string): ContentItem | undefined;
573
577
  /**
574
- * Get a video by ID
575
- * Also updates LRU access time for garbage collection
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
576
591
  */
577
592
  getVideo(id: string): VideoItem | undefined;
578
593
  /**
579
- * Get ordered list of videos
594
+ * @deprecated Use getItems instead
580
595
  */
581
596
  getVideos(): VideoItem[];
582
597
  /**
583
- * Update a video in the feed (for optimistic updates)
598
+ * @deprecated Use updateItem instead
584
599
  */
585
600
  updateVideo(id: string, updates: Partial<VideoItem>): void;
586
- /**
587
- * Replace all items in the feed (e.g. for Playlist synchronization)
588
- */
589
- replaceItems(items: VideoItem[]): void;
590
601
  /**
591
602
  * Remove an item from the feed
592
603
  *
@@ -599,10 +610,10 @@ declare class FeedManager {
599
610
  *
600
611
  * @example
601
612
  * ```typescript
602
- * // User reports a video
603
- * const wasRemoved = feedManager.removeItem(videoId);
613
+ * // User reports a content item
614
+ * const wasRemoved = feedManager.removeItem(itemId);
604
615
  * if (wasRemoved) {
605
- * // Navigate to next video
616
+ * // Navigate to next item
606
617
  * }
607
618
  * ```
608
619
  */
@@ -629,18 +640,18 @@ declare class FeedManager {
629
640
  * Used by LifecycleManager to restore state without API call.
630
641
  * This bypasses normal data flow for state restoration.
631
642
  *
632
- * @param items - Video items from snapshot
643
+ * @param items - Content items from snapshot
633
644
  * @param cursor - Pagination cursor from snapshot
634
645
  * @param options - Additional hydration options
635
646
  */
636
- hydrateFromSnapshot(items: VideoItem[], cursor: string | null, options?: {
647
+ hydrateFromSnapshot(items: ContentItem[], cursor: string | null, options?: {
637
648
  /** Whether to mark data as stale for background revalidation */
638
649
  markAsStale?: boolean;
639
650
  }): void;
640
651
  /**
641
652
  * Save the current recommend feed state before switching to playlist mode.
642
653
  *
643
- * @param activeIndex - Current scroll position (active video index)
654
+ * @param activeIndex - Current scroll position (active item index)
644
655
  */
645
656
  saveRecommendSnapshot(activeIndex: number): void;
646
657
  /**
@@ -658,8 +669,8 @@ declare class FeedManager {
658
669
  * Update prefetch cache with current feed tail
659
670
  * Called automatically after loadInitial() and loadMore()
660
671
  *
661
- * Strategy: Cache the LAST N videos (tail of feed)
662
- * 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
663
674
  */
664
675
  updatePrefetchCache(): void;
665
676
  /**
@@ -698,15 +709,15 @@ declare class FeedManager {
698
709
  */
699
710
  clearPrefetchCache(): Promise<void>;
700
711
  /**
701
- * Evict videos that user has scrolled past from prefetch cache
712
+ * Evict items that user has scrolled past from prefetch cache
702
713
  * Called when user's focusedIndex changes
703
714
  *
704
- * Strategy: Remove all videos at or before current position
705
- * 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
706
717
  *
707
- * @param currentIndex - Current focused video index in feed
718
+ * @param currentIndex - Current focused item index in feed
708
719
  */
709
- evictViewedVideosFromCache(currentIndex: number): Promise<void>;
720
+ evictViewedItemsFromCache(currentIndex: number): Promise<void>;
710
721
  /**
711
722
  * Get prefetch cache configuration (for external access)
712
723
  */
@@ -716,15 +727,15 @@ declare class FeedManager {
716
727
  */
717
728
  private fetchWithRetry;
718
729
  /**
719
- * Add videos with deduplication
730
+ * Add items with deduplication
720
731
  * Triggers garbage collection if cache exceeds maxCacheSize
721
732
  */
722
- private addVideos;
733
+ private addItems;
723
734
  /**
724
- * Merge videos (for SWR revalidation)
725
- * 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
726
737
  */
727
- private mergeVideos;
738
+ private mergeItems;
728
739
  /**
729
740
  * Handle and categorize errors
730
741
  */
@@ -741,9 +752,9 @@ declare class FeedManager {
741
752
  * Run garbage collection using LRU (Least Recently Used) policy
742
753
  *
743
754
  * When cache size exceeds maxCacheSize:
744
- * 1. Sort videos by last access time (oldest first)
745
- * 2. Evict oldest videos until cache is within limit
746
- * 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)
747
758
  *
748
759
  * @returns Number of evicted items
749
760
  */
@@ -1441,7 +1452,7 @@ declare class LifecycleManager {
1441
1452
  * @param data.restoreFrame - Captured video frame at playback position (only saved if restorePlaybackPosition is enabled)
1442
1453
  */
1443
1454
  saveSnapshot(data: {
1444
- items: VideoItem[];
1455
+ items: ContentItem[];
1445
1456
  cursor: string | null;
1446
1457
  focusedIndex: number;
1447
1458
  scrollPosition?: number;
@@ -1640,7 +1651,7 @@ declare class OptimisticManager {
1640
1651
  private readonly config;
1641
1652
  /** Interaction adapter */
1642
1653
  private readonly interaction?;
1643
- /** Feed manager for updating video state */
1654
+ /** Feed manager for updating item state */
1644
1655
  private readonly feedManager?;
1645
1656
  /** Logger adapter */
1646
1657
  private readonly logger?;
@@ -1648,25 +1659,25 @@ declare class OptimisticManager {
1648
1659
  private readonly eventListeners;
1649
1660
  /** Retry timer */
1650
1661
  private retryTimer;
1651
- /** Debounce timers for like/unlike per video */
1662
+ /** Debounce timers for like/unlike per item */
1652
1663
  private readonly likeDebounceTimers;
1653
- /** Intended like state while debouncing (videoId -> isLiked) */
1664
+ /** Intended like state while debouncing (itemId -> isLiked) */
1654
1665
  private readonly intendedLikeState;
1655
1666
  /** Debounce delay in ms */
1656
1667
  private readonly debounceDelay;
1657
1668
  constructor(config?: OptimisticConfig);
1658
1669
  /**
1659
- * Like a video with optimistic update
1670
+ * Like an item with optimistic update
1660
1671
  */
1661
- like(videoId: string): Promise<boolean>;
1672
+ like(itemId: string): Promise<boolean>;
1662
1673
  /**
1663
- * Unlike a video with optimistic update
1674
+ * Unlike an item with optimistic update
1664
1675
  */
1665
- unlike(videoId: string): Promise<boolean>;
1676
+ unlike(itemId: string): Promise<boolean>;
1666
1677
  /**
1667
1678
  * Toggle like state with DEBOUNCE
1668
1679
  */
1669
- toggleLike(videoId: string): void;
1680
+ toggleLike(itemId: string): void;
1670
1681
  /**
1671
1682
  * Execute API call after debounce delay
1672
1683
  */
@@ -1682,9 +1693,9 @@ declare class OptimisticManager {
1682
1693
  /**
1683
1694
  * Toggle follow state
1684
1695
  */
1685
- toggleFollow(videoId: string): Promise<boolean>;
1686
- follow(videoId: string): Promise<boolean>;
1687
- unfollow(videoId: string): Promise<boolean>;
1696
+ toggleFollow(itemId: string): Promise<boolean>;
1697
+ follow(itemId: string): Promise<boolean>;
1698
+ unfollow(itemId: string): Promise<boolean>;
1688
1699
  addEventListener(listener: OptimisticEventListener): () => void;
1689
1700
  removeEventListener(listener: OptimisticEventListener): void;
1690
1701
  /**
@@ -1692,7 +1703,7 @@ declare class OptimisticManager {
1692
1703
  */
1693
1704
  reset(): void;
1694
1705
  getPendingActions(): PendingAction[];
1695
- hasPendingAction(videoId: string, type?: ActionType): boolean;
1706
+ hasPendingAction(itemId: string, type?: ActionType): boolean;
1696
1707
  getFailedQueue(): PendingAction[];
1697
1708
  retryFailed(): Promise<void>;
1698
1709
  clearFailed(): void;
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ var DEFAULT_FEED_CONFIG = {
35
35
  };
36
36
  var DEFAULT_PREFETCH_CACHE_CONFIG = {
37
37
  enabled: true,
38
+ maxItems: 10,
38
39
  maxVideos: 10,
39
40
  storageKey: "sv-prefetch-cache",
40
41
  enableDynamicEviction: true,
@@ -70,10 +71,10 @@ var _FeedManager = class _FeedManager {
70
71
  */
71
72
  this.accessOrder = /* @__PURE__ */ new Map();
72
73
  /**
73
- * Track videos that have already triggered predictive preload
74
+ * Track items that have already triggered predictive preload
74
75
  * to avoid duplicate requests.
75
76
  */
76
- this.preloadedVideoIds = /* @__PURE__ */ new Set();
77
+ this.preloadedItemIds = /* @__PURE__ */ new Set();
77
78
  /**
78
79
  * Recommend feed snapshot — preserved when switching to playlist mode.
79
80
  * Stored here (not in React refs) because FeedManager is a singleton
@@ -246,7 +247,7 @@ var _FeedManager = class _FeedManager {
246
247
  }
247
248
  try {
248
249
  const response = await this.dataSource.fetchFeed();
249
- this.mergeVideos(response.items, true);
250
+ this.mergeItems(response.items, true);
250
251
  this.store.setState({
251
252
  cursor: response.nextCursor,
252
253
  hasMore: response.hasMore,
@@ -259,17 +260,17 @@ var _FeedManager = class _FeedManager {
259
260
  /**
260
261
  * Handle playback progress and trigger predictive preloading
261
262
  *
262
- * @param videoId - ID of the currently playing video
263
+ * @param itemId - ID of the currently playing item
263
264
  * @param progress - Current playback progress (0-1)
264
265
  * @param governor - Resource governor to trigger preload
265
266
  * @param threshold - Progress threshold to trigger preload (default: 0.2)
266
267
  */
267
- handlePlaybackProgress(videoId, progress, governor, threshold = 0.2) {
268
- if (this.preloadedVideoIds.has(videoId)) return;
268
+ handlePlaybackProgress(itemId, progress, governor, threshold = 0.2) {
269
+ if (this.preloadedItemIds.has(itemId)) return;
269
270
  if (progress >= threshold) {
270
- this.preloadedVideoIds.add(videoId);
271
+ this.preloadedItemIds.add(itemId);
271
272
  const state = this.store.getState();
272
- const currentIndex = state.displayOrder.indexOf(videoId);
273
+ const currentIndex = state.displayOrder.indexOf(itemId);
273
274
  if (currentIndex !== -1) {
274
275
  const nextIndices = [currentIndex + 1, currentIndex + 2].filter(
275
276
  (idx) => idx < state.displayOrder.length
@@ -283,28 +284,24 @@ var _FeedManager = class _FeedManager {
283
284
  }
284
285
  }
285
286
  }
286
- /**
287
- * Get a video by ID
288
- * Also updates LRU access time for garbage collection
289
- */
290
- getVideo(id) {
291
- const video = this.store.getState().itemsById.get(id);
292
- if (video) {
287
+ getItem(id) {
288
+ const item = this.store.getState().itemsById.get(id);
289
+ if (item) {
293
290
  this.accessOrder.set(id, Date.now());
294
291
  }
295
- return video;
292
+ return item;
296
293
  }
297
294
  /**
298
- * Get ordered list of videos
295
+ * Get ordered list of items
299
296
  */
300
- getVideos() {
297
+ getItems() {
301
298
  const state = this.store.getState();
302
299
  return state.displayOrder.map((id) => state.itemsById.get(id)).filter((v) => v !== void 0);
303
300
  }
304
301
  /**
305
- * Update a video in the feed (for optimistic updates)
302
+ * Update an item in the feed (for optimistic updates)
306
303
  */
307
- updateVideo(id, updates) {
304
+ updateItem(id, updates) {
308
305
  const state = this.store.getState();
309
306
  const existing = state.itemsById.get(id);
310
307
  if (existing) {
@@ -332,6 +329,24 @@ var _FeedManager = class _FeedManager {
332
329
  hasMore: false
333
330
  });
334
331
  }
332
+ /**
333
+ * @deprecated Use getItem instead
334
+ */
335
+ getVideo(id) {
336
+ return this.getItem(id);
337
+ }
338
+ /**
339
+ * @deprecated Use getItems instead
340
+ */
341
+ getVideos() {
342
+ return this.getItems();
343
+ }
344
+ /**
345
+ * @deprecated Use updateItem instead
346
+ */
347
+ updateVideo(id, updates) {
348
+ this.updateItem(id, updates);
349
+ }
335
350
  /**
336
351
  * Remove an item from the feed
337
352
  *
@@ -344,10 +359,10 @@ var _FeedManager = class _FeedManager {
344
359
  *
345
360
  * @example
346
361
  * ```typescript
347
- * // User reports a video
348
- * const wasRemoved = feedManager.removeItem(videoId);
362
+ * // User reports a content item
363
+ * const wasRemoved = feedManager.removeItem(itemId);
349
364
  * if (wasRemoved) {
350
- * // Navigate to next video
365
+ * // Navigate to next item
351
366
  * }
352
367
  * ```
353
368
  */
@@ -381,7 +396,7 @@ var _FeedManager = class _FeedManager {
381
396
  this.cancelPendingRequests();
382
397
  this.inFlightRequests.clear();
383
398
  this.accessOrder.clear();
384
- this.preloadedVideoIds.clear();
399
+ this.preloadedItemIds.clear();
385
400
  this.store.setState(createInitialState());
386
401
  }
387
402
  /**
@@ -408,7 +423,7 @@ var _FeedManager = class _FeedManager {
408
423
  * Used by LifecycleManager to restore state without API call.
409
424
  * This bypasses normal data flow for state restoration.
410
425
  *
411
- * @param items - Video items from snapshot
426
+ * @param items - Content items from snapshot
412
427
  * @param cursor - Pagination cursor from snapshot
413
428
  * @param options - Additional hydration options
414
429
  */
@@ -419,10 +434,10 @@ var _FeedManager = class _FeedManager {
419
434
  const now = Date.now();
420
435
  const newItemsById = /* @__PURE__ */ new Map();
421
436
  const newDisplayOrder = [];
422
- for (const video of items) {
423
- newItemsById.set(video.id, video);
424
- newDisplayOrder.push(video.id);
425
- this.accessOrder.set(video.id, now);
437
+ for (const item of items) {
438
+ newItemsById.set(item.id, item);
439
+ newDisplayOrder.push(item.id);
440
+ this.accessOrder.set(item.id, now);
426
441
  }
427
442
  this.store.setState({
428
443
  itemsById: newItemsById,
@@ -442,11 +457,11 @@ var _FeedManager = class _FeedManager {
442
457
  /**
443
458
  * Save the current recommend feed state before switching to playlist mode.
444
459
  *
445
- * @param activeIndex - Current scroll position (active video index)
460
+ * @param activeIndex - Current scroll position (active item index)
446
461
  */
447
462
  saveRecommendSnapshot(activeIndex) {
448
463
  this.recommendSnapshot = {
449
- items: this.getVideos(),
464
+ items: this.getItems(),
450
465
  cursor: this.store.getState().cursor,
451
466
  hasMore: this.store.getState().hasMore,
452
467
  activeIndex
@@ -484,20 +499,20 @@ var _FeedManager = class _FeedManager {
484
499
  * Update prefetch cache with current feed tail
485
500
  * Called automatically after loadInitial() and loadMore()
486
501
  *
487
- * Strategy: Cache the LAST N videos (tail of feed)
488
- * These are videos user hasn't seen yet, perfect for instant display
502
+ * Strategy: Cache the LAST N items (tail of feed)
503
+ * These are items user hasn't seen yet, perfect for instant display
489
504
  */
490
505
  updatePrefetchCache() {
491
506
  if (!this.prefetchConfig.enabled) return;
492
507
  if (!this.storage) return;
493
508
  const state = this.store.getState();
494
- const allVideos = this.getVideos();
495
- if (allVideos.length < this.prefetchConfig.maxVideos) {
509
+ const allItems = this.getItems();
510
+ if (allItems.length < this.prefetchConfig.maxItems) {
496
511
  return;
497
512
  }
498
- const tailVideos = allVideos.slice(-this.prefetchConfig.maxVideos);
513
+ const tailItems = allItems.slice(-this.prefetchConfig.maxItems);
499
514
  const cacheData = {
500
- items: tailVideos,
515
+ items: tailItems,
501
516
  savedAt: Date.now(),
502
517
  cursor: state.cursor
503
518
  };
@@ -581,26 +596,26 @@ var _FeedManager = class _FeedManager {
581
596
  }
582
597
  }
583
598
  /**
584
- * Evict videos that user has scrolled past from prefetch cache
599
+ * Evict items that user has scrolled past from prefetch cache
585
600
  * Called when user's focusedIndex changes
586
601
  *
587
- * Strategy: Remove all videos at or before current position
588
- * This ensures user doesn't rewatch videos on reload
602
+ * Strategy: Remove all items at or before current position
603
+ * This ensures user doesn't rewatch items on reload
589
604
  *
590
- * @param currentIndex - Current focused video index in feed
605
+ * @param currentIndex - Current focused item index in feed
591
606
  */
592
- async evictViewedVideosFromCache(currentIndex) {
607
+ async evictViewedItemsFromCache(currentIndex) {
593
608
  if (!this.prefetchConfig.enabled) return;
594
609
  if (!this.prefetchConfig.enableDynamicEviction) return;
595
610
  if (!this.storage) return;
596
611
  try {
597
612
  const cache = await this.loadPrefetchCache();
598
613
  if (!cache || cache.items.length === 0) return;
599
- const allVideos = this.getVideos();
600
- const currentVideo = allVideos[currentIndex];
601
- if (!currentVideo) return;
602
- const viewedVideoIds = new Set(allVideos.slice(0, currentIndex + 1).map((v) => v.id));
603
- const updatedItems = cache.items.filter((item) => !viewedVideoIds.has(item.id));
614
+ const allItems = this.getItems();
615
+ const currentItem = allItems[currentIndex];
616
+ if (!currentItem) return;
617
+ const viewedItemIds = new Set(allItems.slice(0, currentIndex + 1).map((v) => v.id));
618
+ const updatedItems = cache.items.filter((item) => !viewedItemIds.has(item.id));
604
619
  if (updatedItems.length === cache.items.length) return;
605
620
  if (updatedItems.length === 0) {
606
621
  await this.storage.remove(this.prefetchConfig.storageKey);
@@ -636,7 +651,7 @@ var _FeedManager = class _FeedManager {
636
651
  if (replace) {
637
652
  this.replaceItems(response.items);
638
653
  } else {
639
- this.addVideos(response.items);
654
+ this.addItems(response.items);
640
655
  }
641
656
  this.store.setState({
642
657
  cursor: response.nextCursor,
@@ -660,23 +675,23 @@ var _FeedManager = class _FeedManager {
660
675
  throw lastError;
661
676
  }
662
677
  /**
663
- * Add videos with deduplication
678
+ * Add items with deduplication
664
679
  * Triggers garbage collection if cache exceeds maxCacheSize
665
680
  */
666
- addVideos(videos) {
681
+ addItems(items) {
667
682
  const state = this.store.getState();
668
683
  const newItemsById = new Map(state.itemsById);
669
684
  const newDisplayOrder = [...state.displayOrder];
670
685
  const now = Date.now();
671
- for (const video of videos) {
672
- const existing = newItemsById.get(video.id);
686
+ for (const item of items) {
687
+ const existing = newItemsById.get(item.id);
673
688
  if (existing) {
674
- newItemsById.set(video.id, { ...existing, ...video });
689
+ newItemsById.set(item.id, { ...existing, ...item });
675
690
  } else {
676
- newItemsById.set(video.id, video);
677
- newDisplayOrder.push(video.id);
691
+ newItemsById.set(item.id, item);
692
+ newDisplayOrder.push(item.id);
678
693
  }
679
- this.accessOrder.set(video.id, now);
694
+ this.accessOrder.set(item.id, now);
680
695
  }
681
696
  this.store.setState({
682
697
  itemsById: newItemsById,
@@ -687,19 +702,19 @@ var _FeedManager = class _FeedManager {
687
702
  }
688
703
  }
689
704
  /**
690
- * Merge videos (for SWR revalidation)
691
- * Updates existing videos, adds new ones at the beginning
705
+ * Merge items (for SWR revalidation)
706
+ * Updates existing items, adds new ones at the beginning
692
707
  */
693
- mergeVideos(videos, prepend) {
708
+ mergeItems(items, prepend) {
694
709
  const state = this.store.getState();
695
710
  const newItemsById = new Map(state.itemsById);
696
711
  const newIds = [];
697
- for (const video of videos) {
698
- if (newItemsById.has(video.id)) {
699
- newItemsById.set(video.id, video);
712
+ for (const item of items) {
713
+ if (newItemsById.has(item.id)) {
714
+ newItemsById.set(item.id, item);
700
715
  } else {
701
- newItemsById.set(video.id, video);
702
- newIds.push(video.id);
716
+ newItemsById.set(item.id, item);
717
+ newIds.push(item.id);
703
718
  }
704
719
  }
705
720
  const newDisplayOrder = prepend ? [...newIds, ...state.displayOrder] : [...state.displayOrder, ...newIds];
@@ -772,9 +787,9 @@ var _FeedManager = class _FeedManager {
772
787
  * Run garbage collection using LRU (Least Recently Used) policy
773
788
  *
774
789
  * When cache size exceeds maxCacheSize:
775
- * 1. Sort videos by last access time (oldest first)
776
- * 2. Evict oldest videos until cache is within limit
777
- * 3. Keep videos that are currently in viewport (most recent in displayOrder)
790
+ * 1. Sort items by last access time (oldest first)
791
+ * 2. Evict oldest items until cache is within limit
792
+ * 3. Keep items that are currently in viewport (most recent in displayOrder)
778
793
  *
779
794
  * @returns Number of evicted items
780
795
  */
@@ -2653,9 +2668,9 @@ var OptimisticManager = class {
2653
2668
  this.eventListeners = /* @__PURE__ */ new Set();
2654
2669
  /** Retry timer */
2655
2670
  this.retryTimer = null;
2656
- /** Debounce timers for like/unlike per video */
2671
+ /** Debounce timers for like/unlike per item */
2657
2672
  this.likeDebounceTimers = /* @__PURE__ */ new Map();
2658
- /** Intended like state while debouncing (videoId -> isLiked) */
2673
+ /** Intended like state while debouncing (itemId -> isLiked) */
2659
2674
  this.intendedLikeState = /* @__PURE__ */ new Map();
2660
2675
  /** Debounce delay in ms */
2661
2676
  this.debounceDelay = 300;
@@ -2670,73 +2685,75 @@ var OptimisticManager = class {
2670
2685
  // PUBLIC API - ACTIONS
2671
2686
  // ═══════════════════════════════════════════════════════════════
2672
2687
  /**
2673
- * Like a video with optimistic update
2688
+ * Like an item with optimistic update
2674
2689
  */
2675
- async like(videoId) {
2676
- return this.performAction("like", videoId, async () => {
2690
+ async like(itemId) {
2691
+ return this.performAction("like", itemId, async () => {
2677
2692
  if (!this.interaction) throw new Error("No interaction adapter");
2678
- await this.interaction.like(videoId);
2693
+ await this.interaction.like(itemId);
2679
2694
  });
2680
2695
  }
2681
2696
  /**
2682
- * Unlike a video with optimistic update
2697
+ * Unlike an item with optimistic update
2683
2698
  */
2684
- async unlike(videoId) {
2685
- return this.performAction("unlike", videoId, async () => {
2699
+ async unlike(itemId) {
2700
+ return this.performAction("unlike", itemId, async () => {
2686
2701
  if (!this.interaction) throw new Error("No interaction adapter");
2687
- await this.interaction.unlike(videoId);
2702
+ await this.interaction.unlike(itemId);
2688
2703
  });
2689
2704
  }
2690
2705
  /**
2691
2706
  * Toggle like state with DEBOUNCE
2692
2707
  */
2693
- toggleLike(videoId) {
2694
- const video = this.feedManager?.getVideo(videoId);
2695
- if (!video) {
2696
- this.logger?.warn(`[OptimisticManager] Video not found for toggleLike: ${videoId}`);
2708
+ toggleLike(itemId) {
2709
+ const item = this.feedManager?.getItem(itemId);
2710
+ if (!item) {
2711
+ this.logger?.warn(`[OptimisticManager] Item not found for toggleLike: ${itemId}`);
2697
2712
  return;
2698
2713
  }
2699
- const currentIntended = this.intendedLikeState.get(videoId) ?? video.isLiked;
2714
+ const currentIntended = this.intendedLikeState.get(itemId) ?? item.isLiked;
2700
2715
  const newIsLiked = !currentIntended;
2701
- this.intendedLikeState.set(videoId, newIsLiked);
2716
+ this.intendedLikeState.set(itemId, newIsLiked);
2702
2717
  const likeDelta = newIsLiked ? 1 : -1;
2703
- const currentLikesFromStore = video.stats.likes;
2704
- this.feedManager?.updateVideo(videoId, {
2718
+ const currentLikesFromStore = item.stats.likes;
2719
+ this.feedManager?.updateItem(itemId, {
2705
2720
  isLiked: newIsLiked,
2706
- stats: { ...video.stats, likes: Math.max(0, currentLikesFromStore + likeDelta) }
2721
+ stats: { ...item.stats, likes: Math.max(0, currentLikesFromStore + likeDelta) }
2707
2722
  });
2708
- const actionId = `like-debounce-${videoId}`;
2723
+ const actionId = `like-debounce-${itemId}`;
2709
2724
  this.store.setState((state) => {
2710
2725
  const pendingActions = new Map(state.pendingActions);
2711
2726
  pendingActions.set(actionId, {
2712
2727
  id: actionId,
2713
- videoId,
2728
+ videoId: itemId,
2729
+ // Keep property name 'videoId' in PendingAction for compatibility if needed, but using itemId value
2714
2730
  type: newIsLiked ? "like" : "unlike",
2715
2731
  status: "pending",
2716
2732
  timestamp: Date.now(),
2717
2733
  retryCount: 0,
2718
2734
  rollbackData: {
2719
2735
  isLiked: currentIntended,
2720
- stats: { ...video.stats }
2736
+ stats: { ...item.stats }
2721
2737
  }
2738
+ // Cast back to VideoItem if needed for contract
2722
2739
  });
2723
2740
  return { pendingActions, hasPending: true };
2724
2741
  });
2725
- const existingTimer = this.likeDebounceTimers.get(videoId);
2742
+ const existingTimer = this.likeDebounceTimers.get(itemId);
2726
2743
  if (existingTimer) {
2727
2744
  clearTimeout(existingTimer);
2728
2745
  }
2729
2746
  const timer = setTimeout(() => {
2730
- this.likeDebounceTimers.delete(videoId);
2731
- this.executeDebouncedLikeApi(videoId, actionId);
2747
+ this.likeDebounceTimers.delete(itemId);
2748
+ this.executeDebouncedLikeApi(itemId, actionId);
2732
2749
  }, this.debounceDelay);
2733
- this.likeDebounceTimers.set(videoId, timer);
2750
+ this.likeDebounceTimers.set(itemId, timer);
2734
2751
  }
2735
2752
  /**
2736
2753
  * Execute API call after debounce delay
2737
2754
  */
2738
- async executeDebouncedLikeApi(videoId, actionId) {
2739
- const finalIntended = this.intendedLikeState.get(videoId);
2755
+ async executeDebouncedLikeApi(itemId, actionId) {
2756
+ const finalIntended = this.intendedLikeState.get(itemId);
2740
2757
  if (finalIntended === void 0) {
2741
2758
  this.removePendingAction(actionId);
2742
2759
  return;
@@ -2744,15 +2761,15 @@ var OptimisticManager = class {
2744
2761
  try {
2745
2762
  if (!this.interaction) throw new Error("No interaction adapter");
2746
2763
  if (finalIntended) {
2747
- await this.interaction.like(videoId);
2764
+ await this.interaction.like(itemId);
2748
2765
  } else {
2749
- await this.interaction.unlike(videoId);
2766
+ await this.interaction.unlike(itemId);
2750
2767
  }
2751
- if (this.intendedLikeState.get(videoId) === finalIntended) {
2752
- this.intendedLikeState.delete(videoId);
2768
+ if (this.intendedLikeState.get(itemId) === finalIntended) {
2769
+ this.intendedLikeState.delete(itemId);
2753
2770
  }
2754
2771
  } catch (error) {
2755
- this.handleDebouncedApiError(videoId, finalIntended, error);
2772
+ this.handleDebouncedApiError(itemId, finalIntended, error);
2756
2773
  } finally {
2757
2774
  this.removePendingAction(actionId);
2758
2775
  }
@@ -2760,19 +2777,19 @@ var OptimisticManager = class {
2760
2777
  /**
2761
2778
  * Handle errors from debounced API calls
2762
2779
  */
2763
- handleDebouncedApiError(videoId, finalIntended, error) {
2780
+ handleDebouncedApiError(itemId, finalIntended, error) {
2764
2781
  const err = error instanceof Error ? error : new Error(String(error));
2765
- this.logger?.error(`[OptimisticManager] API failed for ${videoId}`, err);
2766
- if (!this.intendedLikeState.has(videoId)) {
2767
- const currentVideo = this.feedManager?.getVideo(videoId);
2768
- if (currentVideo) {
2782
+ this.logger?.error(`[OptimisticManager] API failed for ${itemId}`, err);
2783
+ if (!this.intendedLikeState.has(itemId)) {
2784
+ const currentItem = this.feedManager?.getItem(itemId);
2785
+ if (currentItem) {
2769
2786
  const rollbackIsLiked = !finalIntended;
2770
2787
  const rollbackDelta = rollbackIsLiked ? 1 : -1;
2771
- this.feedManager?.updateVideo(videoId, {
2788
+ this.feedManager?.updateItem(itemId, {
2772
2789
  isLiked: rollbackIsLiked,
2773
2790
  stats: {
2774
- ...currentVideo.stats,
2775
- likes: Math.max(0, currentVideo.stats.likes + rollbackDelta)
2791
+ ...currentItem.stats,
2792
+ likes: Math.max(0, currentItem.stats.likes + rollbackDelta)
2776
2793
  }
2777
2794
  });
2778
2795
  }
@@ -2794,21 +2811,21 @@ var OptimisticManager = class {
2794
2811
  /**
2795
2812
  * Toggle follow state
2796
2813
  */
2797
- async toggleFollow(videoId) {
2798
- const video = this.feedManager?.getVideo(videoId);
2799
- if (!video) return false;
2800
- return video.isFollowing ? this.unfollow(videoId) : this.follow(videoId);
2814
+ async toggleFollow(itemId) {
2815
+ const item = this.feedManager?.getItem(itemId);
2816
+ if (!item) return false;
2817
+ return item.isFollowing ? this.unfollow(itemId) : this.follow(itemId);
2801
2818
  }
2802
- async follow(videoId) {
2803
- return this.performAction("follow", videoId, async () => {
2819
+ async follow(itemId) {
2820
+ return this.performAction("follow", itemId, async () => {
2804
2821
  if (!this.interaction) throw new Error("No interaction adapter");
2805
- await this.interaction.follow(videoId);
2822
+ await this.interaction.follow(itemId);
2806
2823
  });
2807
2824
  }
2808
- async unfollow(videoId) {
2809
- return this.performAction("unfollow", videoId, async () => {
2825
+ async unfollow(itemId) {
2826
+ return this.performAction("unfollow", itemId, async () => {
2810
2827
  if (!this.interaction) throw new Error("No interaction adapter");
2811
- await this.interaction.unfollow(videoId);
2828
+ await this.interaction.unfollow(itemId);
2812
2829
  });
2813
2830
  }
2814
2831
  addEventListener(listener) {
@@ -2830,10 +2847,10 @@ var OptimisticManager = class {
2830
2847
  getPendingActions() {
2831
2848
  return [...this.store.getState().pendingActions.values()];
2832
2849
  }
2833
- hasPendingAction(videoId, type) {
2850
+ hasPendingAction(itemId, type) {
2834
2851
  const actions = this.store.getState().pendingActions;
2835
2852
  for (const action of actions.values()) {
2836
- if (action.videoId === videoId && action.status === "pending" && (!type || action.type === type)) {
2853
+ if (action.videoId === itemId && action.status === "pending" && (!type || action.type === type)) {
2837
2854
  return true;
2838
2855
  }
2839
2856
  }
@@ -2892,23 +2909,23 @@ var OptimisticManager = class {
2892
2909
  }
2893
2910
  }
2894
2911
  }
2895
- async performAction(type, videoId, apiCall) {
2896
- const video = this.feedManager?.getVideo(videoId);
2897
- if (!video) return false;
2898
- if (this.hasPendingAction(videoId, type)) {
2912
+ async performAction(type, itemId, apiCall) {
2913
+ const item = this.feedManager?.getItem(itemId);
2914
+ if (!item) return false;
2915
+ if (this.hasPendingAction(itemId, type)) {
2899
2916
  return false;
2900
2917
  }
2901
2918
  const action = {
2902
2919
  id: generateActionId(),
2903
2920
  type,
2904
- videoId,
2905
- rollbackData: this.createRollbackData(type, video),
2921
+ videoId: itemId,
2922
+ rollbackData: this.createRollbackData(type, item),
2906
2923
  timestamp: Date.now(),
2907
2924
  status: "pending",
2908
2925
  retryCount: 0
2909
2926
  };
2910
2927
  this.addPendingAction(action);
2911
- this.applyOptimisticUpdate(type, video);
2928
+ this.applyOptimisticUpdate(type, item);
2912
2929
  this.emit({ type: "actionStart", action });
2913
2930
  try {
2914
2931
  await apiCall();
@@ -2922,32 +2939,32 @@ var OptimisticManager = class {
2922
2939
  return false;
2923
2940
  }
2924
2941
  }
2925
- createRollbackData(type, video) {
2942
+ createRollbackData(type, item) {
2926
2943
  if (type === "like" || type === "unlike") {
2927
- return { isLiked: video.isLiked, stats: { ...video.stats } };
2944
+ return { isLiked: item.isLiked, stats: { ...item.stats } };
2928
2945
  }
2929
- return { isFollowing: video.isFollowing };
2946
+ return { isFollowing: item.isFollowing };
2930
2947
  }
2931
- applyOptimisticUpdate(type, video) {
2948
+ applyOptimisticUpdate(type, item) {
2932
2949
  if (!this.feedManager) return;
2933
2950
  if (type === "like") {
2934
- this.feedManager.updateVideo(video.id, {
2951
+ this.feedManager.updateItem(item.id, {
2935
2952
  isLiked: true,
2936
- stats: { ...video.stats, likes: video.stats.likes + 1 }
2953
+ stats: { ...item.stats, likes: item.stats.likes + 1 }
2937
2954
  });
2938
2955
  } else if (type === "unlike") {
2939
- this.feedManager.updateVideo(video.id, {
2956
+ this.feedManager.updateItem(item.id, {
2940
2957
  isLiked: false,
2941
- stats: { ...video.stats, likes: Math.max(0, video.stats.likes - 1) }
2958
+ stats: { ...item.stats, likes: Math.max(0, item.stats.likes - 1) }
2942
2959
  });
2943
2960
  } else if (type === "follow") {
2944
- this.feedManager.updateVideo(video.id, { isFollowing: true });
2961
+ this.feedManager.updateItem(item.id, { isFollowing: true });
2945
2962
  } else if (type === "unfollow") {
2946
- this.feedManager.updateVideo(video.id, { isFollowing: false });
2963
+ this.feedManager.updateItem(item.id, { isFollowing: false });
2947
2964
  }
2948
2965
  }
2949
2966
  applyRollback(action) {
2950
- this.feedManager?.updateVideo(action.videoId, action.rollbackData);
2967
+ this.feedManager?.updateItem(action.videoId, action.rollbackData);
2951
2968
  this.emit({ type: "actionRollback", action });
2952
2969
  }
2953
2970
  addPendingAction(action) {
@@ -3005,9 +3022,9 @@ var OptimisticManager = class {
3005
3022
  await this.interaction?.follow(updatedAction.videoId);
3006
3023
  else if (updatedAction.type === "unfollow")
3007
3024
  await this.interaction?.unfollow(updatedAction.videoId);
3008
- const currentVideo = this.feedManager?.getVideo(updatedAction.videoId);
3009
- if (currentVideo) {
3010
- this.applyOptimisticUpdate(updatedAction.type, currentVideo);
3025
+ const currentItem = this.feedManager?.getItem(updatedAction.videoId);
3026
+ if (currentItem) {
3027
+ this.applyOptimisticUpdate(updatedAction.type, currentItem);
3011
3028
  }
3012
3029
  this.markActionSuccess(updatedAction.id);
3013
3030
  } catch (e) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xhub-short/core",
3
3
  "sideEffects": false,
4
- "version": "0.1.0-beta.16",
4
+ "version": "0.1.0-beta.17",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -21,14 +21,14 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "zustand": "^5.0.0",
24
- "@xhub-short/contracts": "0.1.0-beta.16"
24
+ "@xhub-short/contracts": "0.1.0-beta.17"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsup": "^8.3.0",
28
28
  "typescript": "^5.7.0",
29
29
  "vitest": "^2.1.0",
30
- "@xhub-short/tsconfig": "0.0.1-beta.2",
31
- "@xhub-short/vitest-config": "0.1.0-beta.13"
30
+ "@xhub-short/vitest-config": "0.1.0-beta.13",
31
+ "@xhub-short/tsconfig": "0.0.1-beta.2"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsup",