@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +687 -495
  2. package/dist/index.js +367 -60
  3. 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
- this.addVideos(response.items);
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: 1
850
- // Reset speed to normal when loading new video
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 preloadPromises = indicesToPreload.map(async (index) => {
2400
- const videoInfo = this.videoSourceGetter?.(index);
2401
- if (!videoInfo) return;
2402
- try {
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] Preloading video at index ${index} (${videoInfo.id})`
2499
+ `[ResourceGovernor] Preload complete for index ${index} (${result.loadedBytes ?? 0} bytes)`
2405
2500
  );
2406
- const result = await this.videoLoader?.preload(videoInfo.id, videoInfo.source, {
2407
- priority: indicesToPreload.indexOf(index)
2408
- // Lower index = higher priority
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
- await Promise.allSettled(preloadPromises);
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
- 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, ResourceGovernor, calculatePrefetchIndices, calculateWindowIndices, canPause, canPlay, canSeek, computeAllocationChanges, createInitialCommentState, createInitialVideoCommentState, isActiveState, isValidTransition, mapNetworkType };
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.11",
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.11"
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.10"
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",