@xhub-short/core 0.1.0-beta.11 → 0.1.0-beta.12
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 +687 -495
- package/dist/index.js +367 -60
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -54,8 +54,9 @@ var createInitialState = () => ({
|
|
|
54
54
|
lastFetchTime: null
|
|
55
55
|
});
|
|
56
56
|
var _FeedManager = class _FeedManager {
|
|
57
|
-
constructor(dataSource, config = {}, storage, prefetchConfig) {
|
|
57
|
+
constructor(dataSource, config = {}, storage, prefetchConfig, logger) {
|
|
58
58
|
this.dataSource = dataSource;
|
|
59
|
+
this.logger = logger;
|
|
59
60
|
/** Abort controller for cancelling in-flight requests */
|
|
60
61
|
this.abortController = null;
|
|
61
62
|
/**
|
|
@@ -68,6 +69,11 @@ var _FeedManager = class _FeedManager {
|
|
|
68
69
|
* Used for garbage collection
|
|
69
70
|
*/
|
|
70
71
|
this.accessOrder = /* @__PURE__ */ new Map();
|
|
72
|
+
/**
|
|
73
|
+
* Track videos that have already triggered predictive preload
|
|
74
|
+
* to avoid duplicate requests.
|
|
75
|
+
*/
|
|
76
|
+
this.preloadedVideoIds = /* @__PURE__ */ new Set();
|
|
71
77
|
this.config = { ...DEFAULT_FEED_CONFIG, ...config };
|
|
72
78
|
this.prefetchConfig = { ...DEFAULT_PREFETCH_CACHE_CONFIG, ...prefetchConfig };
|
|
73
79
|
this.storage = storage ?? null;
|
|
@@ -112,6 +118,25 @@ var _FeedManager = class _FeedManager {
|
|
|
112
118
|
// ═══════════════════════════════════════════════════════════════
|
|
113
119
|
// PUBLIC API
|
|
114
120
|
// ═══════════════════════════════════════════════════════════════
|
|
121
|
+
/**
|
|
122
|
+
* Get current data source
|
|
123
|
+
*/
|
|
124
|
+
getDataSource() {
|
|
125
|
+
return this.dataSource;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Update data source dynamically
|
|
129
|
+
* Used for switching between Recommendation and Playlist modes
|
|
130
|
+
*
|
|
131
|
+
* @param dataSource - New data source adapter
|
|
132
|
+
* @param options - Options for state transition
|
|
133
|
+
*/
|
|
134
|
+
setDataSource(dataSource, options = { reset: true }) {
|
|
135
|
+
this.dataSource = dataSource;
|
|
136
|
+
if (options.reset) {
|
|
137
|
+
this.reset();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
115
140
|
/**
|
|
116
141
|
* Load initial feed data
|
|
117
142
|
*
|
|
@@ -124,14 +149,14 @@ var _FeedManager = class _FeedManager {
|
|
|
124
149
|
* - If a request for the same cursor is already in-flight, returns the existing Promise
|
|
125
150
|
* - Prevents duplicate API calls from rapid UI interactions
|
|
126
151
|
*/
|
|
127
|
-
async loadInitial() {
|
|
128
|
-
if (_FeedManager.globalMemoryCache) {
|
|
152
|
+
async loadInitial(options) {
|
|
153
|
+
if (!options?.replace && _FeedManager.globalMemoryCache) {
|
|
129
154
|
const { items, nextCursor } = _FeedManager.globalMemoryCache;
|
|
130
155
|
this.hydrateFromSnapshot(items, nextCursor, { markAsStale: false });
|
|
131
156
|
_FeedManager.globalMemoryCache = null;
|
|
132
157
|
return;
|
|
133
158
|
}
|
|
134
|
-
const dedupeKey = "__initial__";
|
|
159
|
+
const dedupeKey = options?.replace ? "__initial_replace__" : "__initial__";
|
|
135
160
|
const existingRequest = this.inFlightRequests.get(dedupeKey);
|
|
136
161
|
if (existingRequest) {
|
|
137
162
|
return existingRequest;
|
|
@@ -141,7 +166,7 @@ var _FeedManager = class _FeedManager {
|
|
|
141
166
|
return;
|
|
142
167
|
}
|
|
143
168
|
this.store.setState({ loading: true, error: null });
|
|
144
|
-
const request = this.executeLoadInitial();
|
|
169
|
+
const request = this.executeLoadInitial(options?.replace);
|
|
145
170
|
this.inFlightRequests.set(dedupeKey, request);
|
|
146
171
|
try {
|
|
147
172
|
await request;
|
|
@@ -152,9 +177,9 @@ var _FeedManager = class _FeedManager {
|
|
|
152
177
|
/**
|
|
153
178
|
* Internal: Execute load initial logic
|
|
154
179
|
*/
|
|
155
|
-
async executeLoadInitial() {
|
|
180
|
+
async executeLoadInitial(replace = false) {
|
|
156
181
|
try {
|
|
157
|
-
await this.fetchWithRetry();
|
|
182
|
+
await this.fetchWithRetry(void 0, replace);
|
|
158
183
|
this.updatePrefetchCache();
|
|
159
184
|
} catch (error) {
|
|
160
185
|
this.handleError(error, "loadInitial");
|
|
@@ -225,6 +250,33 @@ var _FeedManager = class _FeedManager {
|
|
|
225
250
|
} catch {
|
|
226
251
|
}
|
|
227
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Handle playback progress and trigger predictive preloading
|
|
255
|
+
*
|
|
256
|
+
* @param videoId - ID of the currently playing video
|
|
257
|
+
* @param progress - Current playback progress (0-1)
|
|
258
|
+
* @param governor - Resource governor to trigger preload
|
|
259
|
+
* @param threshold - Progress threshold to trigger preload (default: 0.2)
|
|
260
|
+
*/
|
|
261
|
+
handlePlaybackProgress(videoId, progress, governor, threshold = 0.2) {
|
|
262
|
+
if (this.preloadedVideoIds.has(videoId)) return;
|
|
263
|
+
if (progress >= threshold) {
|
|
264
|
+
this.preloadedVideoIds.add(videoId);
|
|
265
|
+
const state = this.store.getState();
|
|
266
|
+
const currentIndex = state.displayOrder.indexOf(videoId);
|
|
267
|
+
if (currentIndex !== -1) {
|
|
268
|
+
const nextIndices = [currentIndex + 1, currentIndex + 2].filter(
|
|
269
|
+
(idx) => idx < state.displayOrder.length
|
|
270
|
+
);
|
|
271
|
+
if (nextIndices.length > 0) {
|
|
272
|
+
governor.triggerPreload(nextIndices);
|
|
273
|
+
this.logger?.debug(
|
|
274
|
+
`[FeedManager] Predictive preload triggered for indices: ${nextIndices.join(", ")}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
228
280
|
/**
|
|
229
281
|
* Get a video by ID
|
|
230
282
|
* Also updates LRU access time for garbage collection
|
|
@@ -255,6 +307,25 @@ var _FeedManager = class _FeedManager {
|
|
|
255
307
|
this.store.setState({ itemsById: newItemsById });
|
|
256
308
|
}
|
|
257
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Replace all items in the feed (e.g. for Playlist synchronization)
|
|
312
|
+
*/
|
|
313
|
+
replaceItems(items) {
|
|
314
|
+
const newItemsById = /* @__PURE__ */ new Map();
|
|
315
|
+
const newDisplayOrder = [];
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
for (const item of items) {
|
|
318
|
+
newItemsById.set(item.id, item);
|
|
319
|
+
newDisplayOrder.push(item.id);
|
|
320
|
+
this.accessOrder.set(item.id, now);
|
|
321
|
+
}
|
|
322
|
+
this.store.setState({
|
|
323
|
+
itemsById: newItemsById,
|
|
324
|
+
displayOrder: newDisplayOrder,
|
|
325
|
+
cursor: null,
|
|
326
|
+
hasMore: false
|
|
327
|
+
});
|
|
328
|
+
}
|
|
258
329
|
/**
|
|
259
330
|
* Remove an item from the feed
|
|
260
331
|
*
|
|
@@ -304,6 +375,7 @@ var _FeedManager = class _FeedManager {
|
|
|
304
375
|
this.cancelPendingRequests();
|
|
305
376
|
this.inFlightRequests.clear();
|
|
306
377
|
this.accessOrder.clear();
|
|
378
|
+
this.preloadedVideoIds.clear();
|
|
307
379
|
this.store.setState(createInitialState());
|
|
308
380
|
}
|
|
309
381
|
/**
|
|
@@ -479,13 +551,17 @@ var _FeedManager = class _FeedManager {
|
|
|
479
551
|
/**
|
|
480
552
|
* Fetch with exponential backoff retry
|
|
481
553
|
*/
|
|
482
|
-
async fetchWithRetry(cursor) {
|
|
554
|
+
async fetchWithRetry(cursor, replace = false) {
|
|
483
555
|
let lastError = null;
|
|
484
556
|
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
485
557
|
try {
|
|
486
558
|
this.abortController = new AbortController();
|
|
487
559
|
const response = await this.dataSource.fetchFeed(cursor);
|
|
488
|
-
|
|
560
|
+
if (replace) {
|
|
561
|
+
this.replaceItems(response.items);
|
|
562
|
+
} else {
|
|
563
|
+
this.addVideos(response.items);
|
|
564
|
+
}
|
|
489
565
|
this.store.setState({
|
|
490
566
|
cursor: response.nextCursor,
|
|
491
567
|
hasMore: response.hasMore,
|
|
@@ -846,8 +922,8 @@ var PlayerEngine = class {
|
|
|
846
922
|
watchTime: 0,
|
|
847
923
|
error: null,
|
|
848
924
|
ended: false,
|
|
849
|
-
playbackRate:
|
|
850
|
-
//
|
|
925
|
+
playbackRate: this.store.getState().playbackRate
|
|
926
|
+
// Persist speed across videos (AC 5)
|
|
851
927
|
});
|
|
852
928
|
this.emitEvent({ type: "videoChange", video });
|
|
853
929
|
this.logger?.debug(`[PlayerEngine] Loaded video: ${video.id}`);
|
|
@@ -1689,25 +1765,7 @@ var LifecycleManager = class {
|
|
|
1689
1765
|
this.store.setState({ isSaving: true });
|
|
1690
1766
|
this.emitEvent({ type: "saveStart" });
|
|
1691
1767
|
try {
|
|
1692
|
-
const snapshot =
|
|
1693
|
-
items: data.items,
|
|
1694
|
-
cursor: data.cursor,
|
|
1695
|
-
focusedIndex: data.focusedIndex,
|
|
1696
|
-
scrollPosition: data.scrollPosition,
|
|
1697
|
-
savedAt: Date.now(),
|
|
1698
|
-
version: this.config.version
|
|
1699
|
-
};
|
|
1700
|
-
if (this.config.restorePlaybackPosition) {
|
|
1701
|
-
if (data.playbackTime !== void 0) {
|
|
1702
|
-
snapshot.playbackTime = data.playbackTime;
|
|
1703
|
-
}
|
|
1704
|
-
if (data.currentVideoId !== void 0) {
|
|
1705
|
-
snapshot.currentVideoId = data.currentVideoId;
|
|
1706
|
-
}
|
|
1707
|
-
if (data.restoreFrame !== void 0) {
|
|
1708
|
-
snapshot.restoreFrame = data.restoreFrame;
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1768
|
+
const snapshot = this.createSnapshot(data);
|
|
1711
1769
|
await this.storage.saveSnapshot(snapshot);
|
|
1712
1770
|
const timestamp = Date.now();
|
|
1713
1771
|
this.store.setState({
|
|
@@ -1848,6 +1906,25 @@ var LifecycleManager = class {
|
|
|
1848
1906
|
isSnapshotStale(snapshot) {
|
|
1849
1907
|
return Date.now() - snapshot.savedAt > this.config.revalidationThresholdMs;
|
|
1850
1908
|
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Create session snapshot from data
|
|
1911
|
+
*/
|
|
1912
|
+
createSnapshot(data) {
|
|
1913
|
+
const snapshot = {
|
|
1914
|
+
items: data.items,
|
|
1915
|
+
cursor: data.cursor,
|
|
1916
|
+
focusedIndex: data.focusedIndex,
|
|
1917
|
+
scrollPosition: data.scrollPosition,
|
|
1918
|
+
savedAt: Date.now(),
|
|
1919
|
+
version: this.config.version
|
|
1920
|
+
};
|
|
1921
|
+
if (this.config.restorePlaybackPosition) {
|
|
1922
|
+
if (data.playbackTime !== void 0) snapshot.playbackTime = data.playbackTime;
|
|
1923
|
+
if (data.currentVideoId !== void 0) snapshot.currentVideoId = data.currentVideoId;
|
|
1924
|
+
if (data.restoreFrame !== void 0) snapshot.restoreFrame = data.restoreFrame;
|
|
1925
|
+
}
|
|
1926
|
+
return snapshot;
|
|
1927
|
+
}
|
|
1851
1928
|
/**
|
|
1852
1929
|
* Emit event to all listeners
|
|
1853
1930
|
*/
|
|
@@ -2396,39 +2473,47 @@ var ResourceGovernor = class {
|
|
|
2396
2473
|
newPreloadingIndices.add(index);
|
|
2397
2474
|
}
|
|
2398
2475
|
this.store.setState({ preloadingIndices: newPreloadingIndices });
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2476
|
+
const maxParallel = 2;
|
|
2477
|
+
const executeInBatches = async () => {
|
|
2478
|
+
for (let i = 0; i < indicesToPreload.length; i += maxParallel) {
|
|
2479
|
+
const batch = indicesToPreload.slice(i, i + maxParallel);
|
|
2480
|
+
await Promise.allSettled(batch.map((index) => this.preloadOne(index, indicesToPreload)));
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
await executeInBatches();
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Internal helper to preload a single video
|
|
2487
|
+
*/
|
|
2488
|
+
async preloadOne(index, indicesToPreload) {
|
|
2489
|
+
const videoInfo = this.videoSourceGetter?.(index);
|
|
2490
|
+
if (!videoInfo) return;
|
|
2491
|
+
try {
|
|
2492
|
+
this.logger?.debug(`[ResourceGovernor] Preloading video at index ${index} (${videoInfo.id})`);
|
|
2493
|
+
const result = await this.videoLoader?.preload(videoInfo.id, videoInfo.source, {
|
|
2494
|
+
priority: indicesToPreload.indexOf(index)
|
|
2495
|
+
// Lower index = higher priority
|
|
2496
|
+
});
|
|
2497
|
+
if (result?.status === "ready") {
|
|
2403
2498
|
this.logger?.debug(
|
|
2404
|
-
`[ResourceGovernor]
|
|
2499
|
+
`[ResourceGovernor] Preload complete for index ${index} (${result.loadedBytes ?? 0} bytes)`
|
|
2405
2500
|
);
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
});
|
|
2410
|
-
if (result?.status === "ready") {
|
|
2411
|
-
this.logger?.debug(
|
|
2412
|
-
`[ResourceGovernor] Preload complete for index ${index} (${result.loadedBytes ?? 0} bytes)`
|
|
2413
|
-
);
|
|
2414
|
-
} else if (result?.status === "error") {
|
|
2415
|
-
this.logger?.warn(
|
|
2416
|
-
`[ResourceGovernor] Preload failed for index ${index}: ${result.error?.message}`
|
|
2417
|
-
);
|
|
2418
|
-
}
|
|
2419
|
-
} catch (err) {
|
|
2420
|
-
this.logger?.error(
|
|
2421
|
-
"[ResourceGovernor] Preload error",
|
|
2422
|
-
err instanceof Error ? err : new Error(String(err))
|
|
2501
|
+
} else if (result?.status === "error") {
|
|
2502
|
+
this.logger?.warn(
|
|
2503
|
+
`[ResourceGovernor] Preload failed for index ${index}: ${result.error?.message}`
|
|
2423
2504
|
);
|
|
2424
|
-
} finally {
|
|
2425
|
-
const currentState = this.store.getState();
|
|
2426
|
-
const updatedPreloadingIndices = new Set(currentState.preloadingIndices);
|
|
2427
|
-
updatedPreloadingIndices.delete(index);
|
|
2428
|
-
this.store.setState({ preloadingIndices: updatedPreloadingIndices });
|
|
2429
2505
|
}
|
|
2430
|
-
})
|
|
2431
|
-
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
this.logger?.error(
|
|
2508
|
+
"[ResourceGovernor] Preload error",
|
|
2509
|
+
err instanceof Error ? err : new Error(String(err))
|
|
2510
|
+
);
|
|
2511
|
+
} finally {
|
|
2512
|
+
const currentState = this.store.getState();
|
|
2513
|
+
const updatedPreloadingIndices = new Set(currentState.preloadingIndices);
|
|
2514
|
+
updatedPreloadingIndices.delete(index);
|
|
2515
|
+
this.store.setState({ preloadingIndices: updatedPreloadingIndices });
|
|
2516
|
+
}
|
|
2432
2517
|
}
|
|
2433
2518
|
/**
|
|
2434
2519
|
* Execute poster preloading for given indices
|
|
@@ -3607,4 +3692,226 @@ var CommentManager = class {
|
|
|
3607
3692
|
}
|
|
3608
3693
|
};
|
|
3609
3694
|
|
|
3610
|
-
|
|
3695
|
+
// src/playlist/types.ts
|
|
3696
|
+
var DEFAULT_PLAYLIST_CONFIG = {
|
|
3697
|
+
metadataWindowSize: 10
|
|
3698
|
+
};
|
|
3699
|
+
|
|
3700
|
+
// src/playlist/PlaylistManager.ts
|
|
3701
|
+
var createInitialState6 = () => ({
|
|
3702
|
+
playlist: null,
|
|
3703
|
+
currentIndex: 0,
|
|
3704
|
+
items: [],
|
|
3705
|
+
loading: false,
|
|
3706
|
+
error: null
|
|
3707
|
+
});
|
|
3708
|
+
var PlaylistManager = class {
|
|
3709
|
+
constructor(dataSource, config = {}, governor) {
|
|
3710
|
+
this.dataSource = dataSource;
|
|
3711
|
+
this.governor = governor;
|
|
3712
|
+
/**
|
|
3713
|
+
* Internal cache of full metadata items.
|
|
3714
|
+
* Items in store.items may be minified to save memory.
|
|
3715
|
+
*/
|
|
3716
|
+
this.fullMetadataItems = [];
|
|
3717
|
+
this.config = { ...DEFAULT_PLAYLIST_CONFIG, ...config };
|
|
3718
|
+
this.store = createStore(createInitialState6);
|
|
3719
|
+
if (this.governor) {
|
|
3720
|
+
this.governorUnsubscribe = this.governor.addEventListener((event) => {
|
|
3721
|
+
if (event.type === "focusChange") {
|
|
3722
|
+
this.jumpTo(event.index);
|
|
3723
|
+
}
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
this.store.subscribe((state, prevState) => {
|
|
3727
|
+
if (state.currentIndex !== prevState.currentIndex || state.playlist !== prevState.playlist) {
|
|
3728
|
+
this.updateMetadataWindow(state.currentIndex);
|
|
3729
|
+
}
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
/**
|
|
3733
|
+
* Load a playlist by ID
|
|
3734
|
+
*/
|
|
3735
|
+
async loadPlaylist(id) {
|
|
3736
|
+
if (!this.dataSource) {
|
|
3737
|
+
this.store.setState({ error: new Error("No playlist data source provided") });
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
this.store.setState({ loading: true, error: null });
|
|
3741
|
+
try {
|
|
3742
|
+
const playlist = await this.dataSource.fetchPlaylist(id);
|
|
3743
|
+
this.setPlaylist(playlist);
|
|
3744
|
+
} catch (error) {
|
|
3745
|
+
this.store.setState({
|
|
3746
|
+
loading: false,
|
|
3747
|
+
error: error instanceof Error ? error : new Error("Failed to load playlist")
|
|
3748
|
+
});
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Set playlist data directly
|
|
3753
|
+
*/
|
|
3754
|
+
setPlaylist(playlist) {
|
|
3755
|
+
this.fullMetadataItems = [...playlist.items];
|
|
3756
|
+
this.updateMetadataWindow(0);
|
|
3757
|
+
this.store.setState({
|
|
3758
|
+
playlist,
|
|
3759
|
+
currentIndex: 0,
|
|
3760
|
+
loading: false,
|
|
3761
|
+
error: null
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* Navigate to next item
|
|
3766
|
+
*/
|
|
3767
|
+
next() {
|
|
3768
|
+
const { currentIndex, items } = this.store.getState();
|
|
3769
|
+
if (currentIndex < items.length - 1) {
|
|
3770
|
+
this.store.setState({ currentIndex: currentIndex + 1 });
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
/**
|
|
3774
|
+
* Navigate to previous item
|
|
3775
|
+
*/
|
|
3776
|
+
prev() {
|
|
3777
|
+
const { currentIndex } = this.store.getState();
|
|
3778
|
+
if (currentIndex > 0) {
|
|
3779
|
+
this.store.setState({ currentIndex: currentIndex - 1 });
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Jump to specific index
|
|
3784
|
+
*/
|
|
3785
|
+
jumpTo(index) {
|
|
3786
|
+
const { items } = this.store.getState();
|
|
3787
|
+
if (index >= 0 && index < items.length) {
|
|
3788
|
+
this.store.setState({ currentIndex: index });
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
/**
|
|
3792
|
+
* Reset state
|
|
3793
|
+
*/
|
|
3794
|
+
reset() {
|
|
3795
|
+
this.fullMetadataItems = [];
|
|
3796
|
+
this.store.setState(createInitialState6());
|
|
3797
|
+
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Destroy the manager
|
|
3800
|
+
*/
|
|
3801
|
+
destroy() {
|
|
3802
|
+
this.governorUnsubscribe?.();
|
|
3803
|
+
this.reset();
|
|
3804
|
+
}
|
|
3805
|
+
/**
|
|
3806
|
+
* Update the sliding window of full metadata items.
|
|
3807
|
+
* Items outside the window are minified to save memory.
|
|
3808
|
+
*/
|
|
3809
|
+
updateMetadataWindow(currentIndex) {
|
|
3810
|
+
const { metadataWindowSize } = this.config;
|
|
3811
|
+
const items = this.fullMetadataItems;
|
|
3812
|
+
if (items.length === 0) return;
|
|
3813
|
+
const halfWindow = Math.floor(metadataWindowSize / 2);
|
|
3814
|
+
let start = Math.max(0, currentIndex - halfWindow);
|
|
3815
|
+
const end = Math.min(items.length, start + metadataWindowSize);
|
|
3816
|
+
if (end === items.length) {
|
|
3817
|
+
start = Math.max(0, end - metadataWindowSize);
|
|
3818
|
+
}
|
|
3819
|
+
const windowedItems = items.map((item, index) => {
|
|
3820
|
+
if (index >= start && index < end) {
|
|
3821
|
+
return item;
|
|
3822
|
+
}
|
|
3823
|
+
return this.minifyItem(item);
|
|
3824
|
+
});
|
|
3825
|
+
this.store.setState({ items: windowedItems });
|
|
3826
|
+
}
|
|
3827
|
+
/**
|
|
3828
|
+
* Create a minified version of a ContentItem to save memory.
|
|
3829
|
+
* Keeps only essential fields for identification and basic UI.
|
|
3830
|
+
*/
|
|
3831
|
+
minifyItem(item) {
|
|
3832
|
+
return {
|
|
3833
|
+
id: item.id,
|
|
3834
|
+
type: item.type,
|
|
3835
|
+
author: {
|
|
3836
|
+
id: item.author.id,
|
|
3837
|
+
name: item.author.name
|
|
3838
|
+
},
|
|
3839
|
+
// Essential stats for basic UI if needed
|
|
3840
|
+
stats: {
|
|
3841
|
+
likes: 0,
|
|
3842
|
+
comments: 0,
|
|
3843
|
+
shares: 0
|
|
3844
|
+
},
|
|
3845
|
+
isLiked: false,
|
|
3846
|
+
isFollowing: false,
|
|
3847
|
+
createdAt: ""
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
|
|
3852
|
+
// src/playlist/PlaylistCollectionManager.ts
|
|
3853
|
+
var createInitialState7 = () => ({
|
|
3854
|
+
playlists: [],
|
|
3855
|
+
loading: false,
|
|
3856
|
+
cursor: null,
|
|
3857
|
+
hasMore: true,
|
|
3858
|
+
error: null
|
|
3859
|
+
});
|
|
3860
|
+
var PlaylistCollectionManager = class {
|
|
3861
|
+
constructor(dataSource) {
|
|
3862
|
+
this.dataSource = dataSource;
|
|
3863
|
+
this.store = createStore(createInitialState7);
|
|
3864
|
+
}
|
|
3865
|
+
/**
|
|
3866
|
+
* Load more playlists (pagination)
|
|
3867
|
+
*/
|
|
3868
|
+
async loadMore() {
|
|
3869
|
+
const { loading, cursor, hasMore } = this.store.getState();
|
|
3870
|
+
if (loading || !hasMore || !this.dataSource) return;
|
|
3871
|
+
this.store.setState({ loading: true, error: null });
|
|
3872
|
+
try {
|
|
3873
|
+
const response = await this.dataSource.fetchPlaylistCollection(cursor || void 0);
|
|
3874
|
+
this.store.setState((state) => ({
|
|
3875
|
+
playlists: [...state.playlists, ...response.playlists],
|
|
3876
|
+
cursor: response.nextCursor,
|
|
3877
|
+
hasMore: response.hasMore,
|
|
3878
|
+
loading: false
|
|
3879
|
+
}));
|
|
3880
|
+
} catch (error) {
|
|
3881
|
+
this.store.setState({
|
|
3882
|
+
loading: false,
|
|
3883
|
+
error: error instanceof Error ? error : new Error("Failed to load playlist collection")
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Refresh the collection (reset and re-fetch)
|
|
3889
|
+
*/
|
|
3890
|
+
async refresh() {
|
|
3891
|
+
if (!this.dataSource) return;
|
|
3892
|
+
this.store.setState({ ...createInitialState7(), loading: true });
|
|
3893
|
+
try {
|
|
3894
|
+
const response = await this.dataSource.fetchPlaylistCollection();
|
|
3895
|
+
this.store.setState({
|
|
3896
|
+
playlists: response.playlists,
|
|
3897
|
+
cursor: response.nextCursor,
|
|
3898
|
+
hasMore: response.hasMore,
|
|
3899
|
+
loading: false,
|
|
3900
|
+
error: null
|
|
3901
|
+
});
|
|
3902
|
+
} catch (error) {
|
|
3903
|
+
this.store.setState({
|
|
3904
|
+
loading: false,
|
|
3905
|
+
error: error instanceof Error ? error : new Error("Failed to refresh playlist collection")
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Reset the store to initial state
|
|
3911
|
+
*/
|
|
3912
|
+
reset() {
|
|
3913
|
+
this.store.setState(createInitialState7());
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
|
|
3917
|
+
export { CommentManager, 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, FeedManager, LifecycleManager, OptimisticManager, PlayerEngine, PlayerStatus, PlaylistCollectionManager, PlaylistManager, ResourceGovernor, calculatePrefetchIndices, calculateWindowIndices, canPause, canPlay, canSeek, computeAllocationChanges, createInitialCommentState, createInitialVideoCommentState, isActiveState, isValidTransition, mapNetworkType };
|
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.12",
|
|
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.12"
|
|
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.0",
|
|
31
|
-
"@xhub-short/vitest-config": "0.1.0-beta.
|
|
30
|
+
"@xhub-short/tsconfig": "0.0.1-beta.0",
|
|
31
|
+
"@xhub-short/vitest-config": "0.1.0-beta.11"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsup",
|