@xhub-short/core 0.1.0-beta.10 → 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 +707 -495
- package/dist/index.js +402 -59
- 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,59 @@ 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
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Remove an item from the feed
|
|
331
|
+
*
|
|
332
|
+
* Used for:
|
|
333
|
+
* - Report: Remove reported content from feed
|
|
334
|
+
* - Not Interested: Remove content user doesn't want to see
|
|
335
|
+
*
|
|
336
|
+
* @param id - Content ID to remove
|
|
337
|
+
* @returns true if item was removed, false if not found
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```typescript
|
|
341
|
+
* // User reports a video
|
|
342
|
+
* const wasRemoved = feedManager.removeItem(videoId);
|
|
343
|
+
* if (wasRemoved) {
|
|
344
|
+
* // Navigate to next video
|
|
345
|
+
* }
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
removeItem(id) {
|
|
349
|
+
const state = this.store.getState();
|
|
350
|
+
if (!state.itemsById.has(id)) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const newItemsById = new Map(state.itemsById);
|
|
354
|
+
newItemsById.delete(id);
|
|
355
|
+
const newDisplayOrder = state.displayOrder.filter((itemId) => itemId !== id);
|
|
356
|
+
this.accessOrder.delete(id);
|
|
357
|
+
this.store.setState({
|
|
358
|
+
itemsById: newItemsById,
|
|
359
|
+
displayOrder: newDisplayOrder
|
|
360
|
+
});
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
258
363
|
/**
|
|
259
364
|
* Check if data is stale and needs revalidation
|
|
260
365
|
*/
|
|
@@ -270,6 +375,7 @@ var _FeedManager = class _FeedManager {
|
|
|
270
375
|
this.cancelPendingRequests();
|
|
271
376
|
this.inFlightRequests.clear();
|
|
272
377
|
this.accessOrder.clear();
|
|
378
|
+
this.preloadedVideoIds.clear();
|
|
273
379
|
this.store.setState(createInitialState());
|
|
274
380
|
}
|
|
275
381
|
/**
|
|
@@ -445,13 +551,17 @@ var _FeedManager = class _FeedManager {
|
|
|
445
551
|
/**
|
|
446
552
|
* Fetch with exponential backoff retry
|
|
447
553
|
*/
|
|
448
|
-
async fetchWithRetry(cursor) {
|
|
554
|
+
async fetchWithRetry(cursor, replace = false) {
|
|
449
555
|
let lastError = null;
|
|
450
556
|
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
451
557
|
try {
|
|
452
558
|
this.abortController = new AbortController();
|
|
453
559
|
const response = await this.dataSource.fetchFeed(cursor);
|
|
454
|
-
|
|
560
|
+
if (replace) {
|
|
561
|
+
this.replaceItems(response.items);
|
|
562
|
+
} else {
|
|
563
|
+
this.addVideos(response.items);
|
|
564
|
+
}
|
|
455
565
|
this.store.setState({
|
|
456
566
|
cursor: response.nextCursor,
|
|
457
567
|
hasMore: response.hasMore,
|
|
@@ -811,7 +921,9 @@ var PlayerEngine = class {
|
|
|
811
921
|
loopCount: 0,
|
|
812
922
|
watchTime: 0,
|
|
813
923
|
error: null,
|
|
814
|
-
ended: false
|
|
924
|
+
ended: false,
|
|
925
|
+
playbackRate: this.store.getState().playbackRate
|
|
926
|
+
// Persist speed across videos (AC 5)
|
|
815
927
|
});
|
|
816
928
|
this.emitEvent({ type: "videoChange", video });
|
|
817
929
|
this.logger?.debug(`[PlayerEngine] Loaded video: ${video.id}`);
|
|
@@ -1653,25 +1765,7 @@ var LifecycleManager = class {
|
|
|
1653
1765
|
this.store.setState({ isSaving: true });
|
|
1654
1766
|
this.emitEvent({ type: "saveStart" });
|
|
1655
1767
|
try {
|
|
1656
|
-
const snapshot =
|
|
1657
|
-
items: data.items,
|
|
1658
|
-
cursor: data.cursor,
|
|
1659
|
-
focusedIndex: data.focusedIndex,
|
|
1660
|
-
scrollPosition: data.scrollPosition,
|
|
1661
|
-
savedAt: Date.now(),
|
|
1662
|
-
version: this.config.version
|
|
1663
|
-
};
|
|
1664
|
-
if (this.config.restorePlaybackPosition) {
|
|
1665
|
-
if (data.playbackTime !== void 0) {
|
|
1666
|
-
snapshot.playbackTime = data.playbackTime;
|
|
1667
|
-
}
|
|
1668
|
-
if (data.currentVideoId !== void 0) {
|
|
1669
|
-
snapshot.currentVideoId = data.currentVideoId;
|
|
1670
|
-
}
|
|
1671
|
-
if (data.restoreFrame !== void 0) {
|
|
1672
|
-
snapshot.restoreFrame = data.restoreFrame;
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1768
|
+
const snapshot = this.createSnapshot(data);
|
|
1675
1769
|
await this.storage.saveSnapshot(snapshot);
|
|
1676
1770
|
const timestamp = Date.now();
|
|
1677
1771
|
this.store.setState({
|
|
@@ -1812,6 +1906,25 @@ var LifecycleManager = class {
|
|
|
1812
1906
|
isSnapshotStale(snapshot) {
|
|
1813
1907
|
return Date.now() - snapshot.savedAt > this.config.revalidationThresholdMs;
|
|
1814
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
|
+
}
|
|
1815
1928
|
/**
|
|
1816
1929
|
* Emit event to all listeners
|
|
1817
1930
|
*/
|
|
@@ -2360,39 +2473,47 @@ var ResourceGovernor = class {
|
|
|
2360
2473
|
newPreloadingIndices.add(index);
|
|
2361
2474
|
}
|
|
2362
2475
|
this.store.setState({ preloadingIndices: newPreloadingIndices });
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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") {
|
|
2367
2498
|
this.logger?.debug(
|
|
2368
|
-
`[ResourceGovernor]
|
|
2499
|
+
`[ResourceGovernor] Preload complete for index ${index} (${result.loadedBytes ?? 0} bytes)`
|
|
2369
2500
|
);
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
});
|
|
2374
|
-
if (result?.status === "ready") {
|
|
2375
|
-
this.logger?.debug(
|
|
2376
|
-
`[ResourceGovernor] Preload complete for index ${index} (${result.loadedBytes ?? 0} bytes)`
|
|
2377
|
-
);
|
|
2378
|
-
} else if (result?.status === "error") {
|
|
2379
|
-
this.logger?.warn(
|
|
2380
|
-
`[ResourceGovernor] Preload failed for index ${index}: ${result.error?.message}`
|
|
2381
|
-
);
|
|
2382
|
-
}
|
|
2383
|
-
} catch (err) {
|
|
2384
|
-
this.logger?.error(
|
|
2385
|
-
"[ResourceGovernor] Preload error",
|
|
2386
|
-
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}`
|
|
2387
2504
|
);
|
|
2388
|
-
} finally {
|
|
2389
|
-
const currentState = this.store.getState();
|
|
2390
|
-
const updatedPreloadingIndices = new Set(currentState.preloadingIndices);
|
|
2391
|
-
updatedPreloadingIndices.delete(index);
|
|
2392
|
-
this.store.setState({ preloadingIndices: updatedPreloadingIndices });
|
|
2393
2505
|
}
|
|
2394
|
-
})
|
|
2395
|
-
|
|
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
|
+
}
|
|
2396
2517
|
}
|
|
2397
2518
|
/**
|
|
2398
2519
|
* Execute poster preloading for given indices
|
|
@@ -3571,4 +3692,226 @@ var CommentManager = class {
|
|
|
3571
3692
|
}
|
|
3572
3693
|
};
|
|
3573
3694
|
|
|
3574
|
-
|
|
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.0
|
|
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",
|