@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.
- package/dist/index.d.ts +69 -58
- package/dist/index.js +158 -141
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INetworkAdapter, IVideoLoader, IPosterLoader, ILogger, VideoSource,
|
|
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
|
|
317
|
-
itemsById: Map<string,
|
|
318
|
-
/** Ordered list of
|
|
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
|
|
364
|
-
* When exceeded, older
|
|
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
|
|
402
|
+
* Maximum number of items to cache
|
|
403
403
|
* Higher = more instant content, but more storage
|
|
404
|
-
* @default 10
|
|
405
404
|
*/
|
|
406
|
-
|
|
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
|
|
415
|
-
* Prevents user from rewatching same
|
|
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
|
|
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
|
|
484
|
+
* Track items that have already triggered predictive preload
|
|
482
485
|
* to avoid duplicate requests.
|
|
483
486
|
*/
|
|
484
|
-
private
|
|
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
|
|
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(
|
|
575
|
+
handlePlaybackProgress(itemId: string, progress: number, governor: ResourceGovernor, threshold?: number): void;
|
|
576
|
+
getItem(id: string): ContentItem | undefined;
|
|
573
577
|
/**
|
|
574
|
-
* Get
|
|
575
|
-
|
|
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
|
-
*
|
|
594
|
+
* @deprecated Use getItems instead
|
|
580
595
|
*/
|
|
581
596
|
getVideos(): VideoItem[];
|
|
582
597
|
/**
|
|
583
|
-
*
|
|
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
|
|
603
|
-
* const wasRemoved = feedManager.removeItem(
|
|
613
|
+
* // User reports a content item
|
|
614
|
+
* const wasRemoved = feedManager.removeItem(itemId);
|
|
604
615
|
* if (wasRemoved) {
|
|
605
|
-
* // Navigate to next
|
|
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 -
|
|
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:
|
|
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
|
|
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
|
|
662
|
-
* These are
|
|
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
|
|
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
|
|
705
|
-
* This ensures user doesn't rewatch
|
|
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
|
|
718
|
+
* @param currentIndex - Current focused item index in feed
|
|
708
719
|
*/
|
|
709
|
-
|
|
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
|
|
730
|
+
* Add items with deduplication
|
|
720
731
|
* Triggers garbage collection if cache exceeds maxCacheSize
|
|
721
732
|
*/
|
|
722
|
-
private
|
|
733
|
+
private addItems;
|
|
723
734
|
/**
|
|
724
|
-
* Merge
|
|
725
|
-
* Updates existing
|
|
735
|
+
* Merge items (for SWR revalidation)
|
|
736
|
+
* Updates existing items, adds new ones at the beginning
|
|
726
737
|
*/
|
|
727
|
-
private
|
|
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
|
|
745
|
-
* 2. Evict oldest
|
|
746
|
-
* 3. Keep
|
|
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:
|
|
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
|
|
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
|
|
1662
|
+
/** Debounce timers for like/unlike per item */
|
|
1652
1663
|
private readonly likeDebounceTimers;
|
|
1653
|
-
/** Intended like state while debouncing (
|
|
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
|
|
1670
|
+
* Like an item with optimistic update
|
|
1660
1671
|
*/
|
|
1661
|
-
like(
|
|
1672
|
+
like(itemId: string): Promise<boolean>;
|
|
1662
1673
|
/**
|
|
1663
|
-
* Unlike
|
|
1674
|
+
* Unlike an item with optimistic update
|
|
1664
1675
|
*/
|
|
1665
|
-
unlike(
|
|
1676
|
+
unlike(itemId: string): Promise<boolean>;
|
|
1666
1677
|
/**
|
|
1667
1678
|
* Toggle like state with DEBOUNCE
|
|
1668
1679
|
*/
|
|
1669
|
-
toggleLike(
|
|
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(
|
|
1686
|
-
follow(
|
|
1687
|
-
unfollow(
|
|
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(
|
|
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
|
|
74
|
+
* Track items that have already triggered predictive preload
|
|
74
75
|
* to avoid duplicate requests.
|
|
75
76
|
*/
|
|
76
|
-
this.
|
|
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.
|
|
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
|
|
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(
|
|
268
|
-
if (this.
|
|
268
|
+
handlePlaybackProgress(itemId, progress, governor, threshold = 0.2) {
|
|
269
|
+
if (this.preloadedItemIds.has(itemId)) return;
|
|
269
270
|
if (progress >= threshold) {
|
|
270
|
-
this.
|
|
271
|
+
this.preloadedItemIds.add(itemId);
|
|
271
272
|
const state = this.store.getState();
|
|
272
|
-
const currentIndex = state.displayOrder.indexOf(
|
|
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
|
-
|
|
288
|
-
|
|
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
|
|
292
|
+
return item;
|
|
296
293
|
}
|
|
297
294
|
/**
|
|
298
|
-
* Get ordered list of
|
|
295
|
+
* Get ordered list of items
|
|
299
296
|
*/
|
|
300
|
-
|
|
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
|
|
302
|
+
* Update an item in the feed (for optimistic updates)
|
|
306
303
|
*/
|
|
307
|
-
|
|
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
|
|
348
|
-
* const wasRemoved = feedManager.removeItem(
|
|
362
|
+
* // User reports a content item
|
|
363
|
+
* const wasRemoved = feedManager.removeItem(itemId);
|
|
349
364
|
* if (wasRemoved) {
|
|
350
|
-
* // Navigate to next
|
|
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.
|
|
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 -
|
|
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
|
|
423
|
-
newItemsById.set(
|
|
424
|
-
newDisplayOrder.push(
|
|
425
|
-
this.accessOrder.set(
|
|
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
|
|
460
|
+
* @param activeIndex - Current scroll position (active item index)
|
|
446
461
|
*/
|
|
447
462
|
saveRecommendSnapshot(activeIndex) {
|
|
448
463
|
this.recommendSnapshot = {
|
|
449
|
-
items: this.
|
|
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
|
|
488
|
-
* These are
|
|
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
|
|
495
|
-
if (
|
|
509
|
+
const allItems = this.getItems();
|
|
510
|
+
if (allItems.length < this.prefetchConfig.maxItems) {
|
|
496
511
|
return;
|
|
497
512
|
}
|
|
498
|
-
const
|
|
513
|
+
const tailItems = allItems.slice(-this.prefetchConfig.maxItems);
|
|
499
514
|
const cacheData = {
|
|
500
|
-
items:
|
|
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
|
|
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
|
|
588
|
-
* This ensures user doesn't rewatch
|
|
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
|
|
605
|
+
* @param currentIndex - Current focused item index in feed
|
|
591
606
|
*/
|
|
592
|
-
async
|
|
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
|
|
600
|
-
const
|
|
601
|
-
if (!
|
|
602
|
-
const
|
|
603
|
-
const updatedItems = cache.items.filter((item) => !
|
|
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.
|
|
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
|
|
678
|
+
* Add items with deduplication
|
|
664
679
|
* Triggers garbage collection if cache exceeds maxCacheSize
|
|
665
680
|
*/
|
|
666
|
-
|
|
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
|
|
672
|
-
const existing = newItemsById.get(
|
|
686
|
+
for (const item of items) {
|
|
687
|
+
const existing = newItemsById.get(item.id);
|
|
673
688
|
if (existing) {
|
|
674
|
-
newItemsById.set(
|
|
689
|
+
newItemsById.set(item.id, { ...existing, ...item });
|
|
675
690
|
} else {
|
|
676
|
-
newItemsById.set(
|
|
677
|
-
newDisplayOrder.push(
|
|
691
|
+
newItemsById.set(item.id, item);
|
|
692
|
+
newDisplayOrder.push(item.id);
|
|
678
693
|
}
|
|
679
|
-
this.accessOrder.set(
|
|
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
|
|
691
|
-
* Updates existing
|
|
705
|
+
* Merge items (for SWR revalidation)
|
|
706
|
+
* Updates existing items, adds new ones at the beginning
|
|
692
707
|
*/
|
|
693
|
-
|
|
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
|
|
698
|
-
if (newItemsById.has(
|
|
699
|
-
newItemsById.set(
|
|
712
|
+
for (const item of items) {
|
|
713
|
+
if (newItemsById.has(item.id)) {
|
|
714
|
+
newItemsById.set(item.id, item);
|
|
700
715
|
} else {
|
|
701
|
-
newItemsById.set(
|
|
702
|
-
newIds.push(
|
|
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
|
|
776
|
-
* 2. Evict oldest
|
|
777
|
-
* 3. Keep
|
|
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
|
|
2671
|
+
/** Debounce timers for like/unlike per item */
|
|
2657
2672
|
this.likeDebounceTimers = /* @__PURE__ */ new Map();
|
|
2658
|
-
/** Intended like state while debouncing (
|
|
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
|
|
2688
|
+
* Like an item with optimistic update
|
|
2674
2689
|
*/
|
|
2675
|
-
async like(
|
|
2676
|
-
return this.performAction("like",
|
|
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(
|
|
2693
|
+
await this.interaction.like(itemId);
|
|
2679
2694
|
});
|
|
2680
2695
|
}
|
|
2681
2696
|
/**
|
|
2682
|
-
* Unlike
|
|
2697
|
+
* Unlike an item with optimistic update
|
|
2683
2698
|
*/
|
|
2684
|
-
async unlike(
|
|
2685
|
-
return this.performAction("unlike",
|
|
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(
|
|
2702
|
+
await this.interaction.unlike(itemId);
|
|
2688
2703
|
});
|
|
2689
2704
|
}
|
|
2690
2705
|
/**
|
|
2691
2706
|
* Toggle like state with DEBOUNCE
|
|
2692
2707
|
*/
|
|
2693
|
-
toggleLike(
|
|
2694
|
-
const
|
|
2695
|
-
if (!
|
|
2696
|
-
this.logger?.warn(`[OptimisticManager]
|
|
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(
|
|
2714
|
+
const currentIntended = this.intendedLikeState.get(itemId) ?? item.isLiked;
|
|
2700
2715
|
const newIsLiked = !currentIntended;
|
|
2701
|
-
this.intendedLikeState.set(
|
|
2716
|
+
this.intendedLikeState.set(itemId, newIsLiked);
|
|
2702
2717
|
const likeDelta = newIsLiked ? 1 : -1;
|
|
2703
|
-
const currentLikesFromStore =
|
|
2704
|
-
this.feedManager?.
|
|
2718
|
+
const currentLikesFromStore = item.stats.likes;
|
|
2719
|
+
this.feedManager?.updateItem(itemId, {
|
|
2705
2720
|
isLiked: newIsLiked,
|
|
2706
|
-
stats: { ...
|
|
2721
|
+
stats: { ...item.stats, likes: Math.max(0, currentLikesFromStore + likeDelta) }
|
|
2707
2722
|
});
|
|
2708
|
-
const actionId = `like-debounce-${
|
|
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: { ...
|
|
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(
|
|
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(
|
|
2731
|
-
this.executeDebouncedLikeApi(
|
|
2747
|
+
this.likeDebounceTimers.delete(itemId);
|
|
2748
|
+
this.executeDebouncedLikeApi(itemId, actionId);
|
|
2732
2749
|
}, this.debounceDelay);
|
|
2733
|
-
this.likeDebounceTimers.set(
|
|
2750
|
+
this.likeDebounceTimers.set(itemId, timer);
|
|
2734
2751
|
}
|
|
2735
2752
|
/**
|
|
2736
2753
|
* Execute API call after debounce delay
|
|
2737
2754
|
*/
|
|
2738
|
-
async executeDebouncedLikeApi(
|
|
2739
|
-
const finalIntended = this.intendedLikeState.get(
|
|
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(
|
|
2764
|
+
await this.interaction.like(itemId);
|
|
2748
2765
|
} else {
|
|
2749
|
-
await this.interaction.unlike(
|
|
2766
|
+
await this.interaction.unlike(itemId);
|
|
2750
2767
|
}
|
|
2751
|
-
if (this.intendedLikeState.get(
|
|
2752
|
-
this.intendedLikeState.delete(
|
|
2768
|
+
if (this.intendedLikeState.get(itemId) === finalIntended) {
|
|
2769
|
+
this.intendedLikeState.delete(itemId);
|
|
2753
2770
|
}
|
|
2754
2771
|
} catch (error) {
|
|
2755
|
-
this.handleDebouncedApiError(
|
|
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(
|
|
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 ${
|
|
2766
|
-
if (!this.intendedLikeState.has(
|
|
2767
|
-
const
|
|
2768
|
-
if (
|
|
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?.
|
|
2788
|
+
this.feedManager?.updateItem(itemId, {
|
|
2772
2789
|
isLiked: rollbackIsLiked,
|
|
2773
2790
|
stats: {
|
|
2774
|
-
...
|
|
2775
|
-
likes: Math.max(0,
|
|
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(
|
|
2798
|
-
const
|
|
2799
|
-
if (!
|
|
2800
|
-
return
|
|
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(
|
|
2803
|
-
return this.performAction("follow",
|
|
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(
|
|
2822
|
+
await this.interaction.follow(itemId);
|
|
2806
2823
|
});
|
|
2807
2824
|
}
|
|
2808
|
-
async unfollow(
|
|
2809
|
-
return this.performAction("unfollow",
|
|
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(
|
|
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(
|
|
2850
|
+
hasPendingAction(itemId, type) {
|
|
2834
2851
|
const actions = this.store.getState().pendingActions;
|
|
2835
2852
|
for (const action of actions.values()) {
|
|
2836
|
-
if (action.videoId ===
|
|
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,
|
|
2896
|
-
const
|
|
2897
|
-
if (!
|
|
2898
|
-
if (this.hasPendingAction(
|
|
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,
|
|
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,
|
|
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,
|
|
2942
|
+
createRollbackData(type, item) {
|
|
2926
2943
|
if (type === "like" || type === "unlike") {
|
|
2927
|
-
return { isLiked:
|
|
2944
|
+
return { isLiked: item.isLiked, stats: { ...item.stats } };
|
|
2928
2945
|
}
|
|
2929
|
-
return { isFollowing:
|
|
2946
|
+
return { isFollowing: item.isFollowing };
|
|
2930
2947
|
}
|
|
2931
|
-
applyOptimisticUpdate(type,
|
|
2948
|
+
applyOptimisticUpdate(type, item) {
|
|
2932
2949
|
if (!this.feedManager) return;
|
|
2933
2950
|
if (type === "like") {
|
|
2934
|
-
this.feedManager.
|
|
2951
|
+
this.feedManager.updateItem(item.id, {
|
|
2935
2952
|
isLiked: true,
|
|
2936
|
-
stats: { ...
|
|
2953
|
+
stats: { ...item.stats, likes: item.stats.likes + 1 }
|
|
2937
2954
|
});
|
|
2938
2955
|
} else if (type === "unlike") {
|
|
2939
|
-
this.feedManager.
|
|
2956
|
+
this.feedManager.updateItem(item.id, {
|
|
2940
2957
|
isLiked: false,
|
|
2941
|
-
stats: { ...
|
|
2958
|
+
stats: { ...item.stats, likes: Math.max(0, item.stats.likes - 1) }
|
|
2942
2959
|
});
|
|
2943
2960
|
} else if (type === "follow") {
|
|
2944
|
-
this.feedManager.
|
|
2961
|
+
this.feedManager.updateItem(item.id, { isFollowing: true });
|
|
2945
2962
|
} else if (type === "unfollow") {
|
|
2946
|
-
this.feedManager.
|
|
2963
|
+
this.feedManager.updateItem(item.id, { isFollowing: false });
|
|
2947
2964
|
}
|
|
2948
2965
|
}
|
|
2949
2966
|
applyRollback(action) {
|
|
2950
|
-
this.feedManager?.
|
|
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
|
|
3009
|
-
if (
|
|
3010
|
-
this.applyOptimisticUpdate(updatedAction.type,
|
|
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.
|
|
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.
|
|
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/
|
|
31
|
-
"@xhub-short/
|
|
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",
|