@xhub-short/core 0.1.0-beta.13 → 0.1.0-beta.14

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 CHANGED
@@ -1632,6 +1632,10 @@ declare class OptimisticManager {
1632
1632
  * Execute API call after debounce delay
1633
1633
  */
1634
1634
  private executeDebouncedLikeApi;
1635
+ /**
1636
+ * Handle errors from debounced API calls
1637
+ */
1638
+ private handleDebouncedApiError;
1635
1639
  /**
1636
1640
  * Remove a pending action by ID
1637
1641
  */
@@ -1642,12 +1646,19 @@ declare class OptimisticManager {
1642
1646
  toggleFollow(videoId: string): Promise<boolean>;
1643
1647
  follow(videoId: string): Promise<boolean>;
1644
1648
  unfollow(videoId: string): Promise<boolean>;
1649
+ addEventListener(listener: OptimisticEventListener): () => void;
1650
+ removeEventListener(listener: OptimisticEventListener): void;
1651
+ /**
1652
+ * Reset all optimistic state
1653
+ */
1654
+ reset(): void;
1645
1655
  getPendingActions(): PendingAction[];
1646
1656
  hasPendingAction(videoId: string, type?: ActionType): boolean;
1647
1657
  getFailedQueue(): PendingAction[];
1648
1658
  retryFailed(): Promise<void>;
1649
1659
  clearFailed(): void;
1650
1660
  destroy(): void;
1661
+ private emit;
1651
1662
  private performAction;
1652
1663
  private createRollbackData;
1653
1664
  private applyOptimisticUpdate;
package/dist/index.js CHANGED
@@ -2675,27 +2675,33 @@ var OptimisticManager = class {
2675
2675
  if (this.intendedLikeState.get(videoId) === finalIntended) {
2676
2676
  this.intendedLikeState.delete(videoId);
2677
2677
  }
2678
- this.removePendingAction(actionId);
2679
2678
  } catch (error) {
2680
- const err = error instanceof Error ? error : new Error(String(error));
2681
- this.logger?.error(`[OptimisticManager] API failed for ${videoId}`, err);
2682
- if (!this.intendedLikeState.has(videoId)) {
2683
- const currentVideo = this.feedManager?.getVideo(videoId);
2684
- if (currentVideo) {
2685
- const rollbackIsLiked = !finalIntended;
2686
- const rollbackDelta = rollbackIsLiked ? 1 : -1;
2687
- this.feedManager?.updateVideo(videoId, {
2688
- isLiked: rollbackIsLiked,
2689
- stats: {
2690
- ...currentVideo.stats,
2691
- likes: Math.max(0, currentVideo.stats.likes + rollbackDelta)
2692
- }
2693
- });
2694
- }
2695
- }
2679
+ this.handleDebouncedApiError(videoId, finalIntended, error);
2680
+ } finally {
2696
2681
  this.removePendingAction(actionId);
2697
2682
  }
2698
2683
  }
2684
+ /**
2685
+ * Handle errors from debounced API calls
2686
+ */
2687
+ handleDebouncedApiError(videoId, finalIntended, error) {
2688
+ const err = error instanceof Error ? error : new Error(String(error));
2689
+ this.logger?.error(`[OptimisticManager] API failed for ${videoId}`, err);
2690
+ if (!this.intendedLikeState.has(videoId)) {
2691
+ const currentVideo = this.feedManager?.getVideo(videoId);
2692
+ if (currentVideo) {
2693
+ const rollbackIsLiked = !finalIntended;
2694
+ const rollbackDelta = rollbackIsLiked ? 1 : -1;
2695
+ this.feedManager?.updateVideo(videoId, {
2696
+ isLiked: rollbackIsLiked,
2697
+ stats: {
2698
+ ...currentVideo.stats,
2699
+ likes: Math.max(0, currentVideo.stats.likes + rollbackDelta)
2700
+ }
2701
+ });
2702
+ }
2703
+ }
2704
+ }
2699
2705
  /**
2700
2706
  * Remove a pending action by ID
2701
2707
  */
@@ -2729,6 +2735,19 @@ var OptimisticManager = class {
2729
2735
  await this.interaction.unfollow(videoId);
2730
2736
  });
2731
2737
  }
2738
+ addEventListener(listener) {
2739
+ this.eventListeners.add(listener);
2740
+ return () => this.removeEventListener(listener);
2741
+ }
2742
+ removeEventListener(listener) {
2743
+ this.eventListeners.delete(listener);
2744
+ }
2745
+ /**
2746
+ * Reset all optimistic state
2747
+ */
2748
+ reset() {
2749
+ this.store.setState(createInitialState5());
2750
+ }
2732
2751
  // ═══════════════════════════════════════════════════════════════
2733
2752
  // STATE MANAGEMENT
2734
2753
  // ═══════════════════════════════════════════════════════════════
@@ -2754,9 +2773,12 @@ var OptimisticManager = class {
2754
2773
  this.store.setState({ isRetrying: true });
2755
2774
  const queue = [...state.failedQueue];
2756
2775
  for (const id of queue) {
2757
- const action = state.pendingActions.get(id);
2758
- if (action && action.retryCount < this.config.maxRetries) {
2776
+ const action = this.store.getState().pendingActions.get(id);
2777
+ if (!action) continue;
2778
+ if (action.retryCount < this.config.maxRetries) {
2759
2779
  await this.retryAction(action);
2780
+ } else {
2781
+ this.emit({ type: "retryExhausted", action });
2760
2782
  }
2761
2783
  }
2762
2784
  this.store.setState({ isRetrying: false });
@@ -2784,9 +2806,22 @@ var OptimisticManager = class {
2784
2806
  // ═══════════════════════════════════════════════════════════════
2785
2807
  // PRIVATE UTILS
2786
2808
  // ═══════════════════════════════════════════════════════════════
2809
+ emit(event) {
2810
+ for (const listener of this.eventListeners) {
2811
+ try {
2812
+ listener(event);
2813
+ } catch (err) {
2814
+ const error = err instanceof Error ? err : new Error(String(err));
2815
+ this.logger?.error("[OptimisticManager] Listener failed", error);
2816
+ }
2817
+ }
2818
+ }
2787
2819
  async performAction(type, videoId, apiCall) {
2788
2820
  const video = this.feedManager?.getVideo(videoId);
2789
2821
  if (!video) return false;
2822
+ if (this.hasPendingAction(videoId, type)) {
2823
+ return false;
2824
+ }
2790
2825
  const action = {
2791
2826
  id: generateActionId(),
2792
2827
  type,
@@ -2798,6 +2833,7 @@ var OptimisticManager = class {
2798
2833
  };
2799
2834
  this.addPendingAction(action);
2800
2835
  this.applyOptimisticUpdate(type, video);
2836
+ this.emit({ type: "actionStart", action });
2801
2837
  try {
2802
2838
  await apiCall();
2803
2839
  this.markActionSuccess(action.id);
@@ -2836,6 +2872,7 @@ var OptimisticManager = class {
2836
2872
  }
2837
2873
  applyRollback(action) {
2838
2874
  this.feedManager?.updateVideo(action.videoId, action.rollbackData);
2875
+ this.emit({ type: "actionRollback", action });
2839
2876
  }
2840
2877
  addPendingAction(action) {
2841
2878
  this.store.setState((state) => {
@@ -2847,6 +2884,10 @@ var OptimisticManager = class {
2847
2884
  markActionSuccess(id) {
2848
2885
  this.store.setState((state) => {
2849
2886
  const m = new Map(state.pendingActions);
2887
+ const action = m.get(id);
2888
+ if (action) {
2889
+ this.emit({ type: "actionSuccess", action: { ...action, status: "success" } });
2890
+ }
2850
2891
  m.delete(id);
2851
2892
  const q = state.failedQueue.filter((x) => x !== id);
2852
2893
  return { pendingActions: m, failedQueue: q, hasPending: m.size > 0 };
@@ -2857,21 +2898,45 @@ var OptimisticManager = class {
2857
2898
  const m = new Map(state.pendingActions);
2858
2899
  const a = m.get(id);
2859
2900
  if (!a) return state;
2860
- m.set(id, { ...a, status: "failed", error });
2901
+ const failedAction = { ...a, status: "failed", error };
2902
+ m.set(id, failedAction);
2861
2903
  const q = state.failedQueue.includes(id) ? state.failedQueue : [...state.failedQueue, id];
2904
+ this.emit({
2905
+ type: "actionFailed",
2906
+ action: failedAction,
2907
+ error: new Error(error)
2908
+ });
2862
2909
  return { pendingActions: m, failedQueue: q };
2863
2910
  });
2864
2911
  }
2865
2912
  async retryAction(action) {
2913
+ this.store.setState((state) => {
2914
+ const m = new Map(state.pendingActions);
2915
+ const a = m.get(action.id);
2916
+ if (a) {
2917
+ m.set(action.id, { ...a, retryCount: a.retryCount + 1 });
2918
+ }
2919
+ return { pendingActions: m };
2920
+ });
2921
+ const updatedAction = this.store.getState().pendingActions.get(action.id);
2922
+ if (!updatedAction) return;
2923
+ this.emit({ type: "retryStart", actionId: updatedAction.id });
2866
2924
  try {
2867
- if (action.type === "like") await this.interaction?.like(action.videoId);
2868
- else if (action.type === "unlike") await this.interaction?.unlike(action.videoId);
2869
- else if (action.type === "follow") await this.interaction?.follow(action.videoId);
2870
- else if (action.type === "unfollow") await this.interaction?.unfollow(action.videoId);
2871
- this.markActionSuccess(action.id);
2925
+ if (updatedAction.type === "like") await this.interaction?.like(updatedAction.videoId);
2926
+ else if (updatedAction.type === "unlike")
2927
+ await this.interaction?.unlike(updatedAction.videoId);
2928
+ else if (updatedAction.type === "follow")
2929
+ await this.interaction?.follow(updatedAction.videoId);
2930
+ else if (updatedAction.type === "unfollow")
2931
+ await this.interaction?.unfollow(updatedAction.videoId);
2932
+ const currentVideo = this.feedManager?.getVideo(updatedAction.videoId);
2933
+ if (currentVideo) {
2934
+ this.applyOptimisticUpdate(updatedAction.type, currentVideo);
2935
+ }
2936
+ this.markActionSuccess(updatedAction.id);
2872
2937
  } catch (e) {
2873
- this.markActionFailed(action.id, String(e));
2874
- this.applyRollback(action);
2938
+ this.markActionFailed(updatedAction.id, String(e));
2939
+ this.applyRollback(updatedAction);
2875
2940
  }
2876
2941
  }
2877
2942
  scheduleRetry() {
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.13",
4
+ "version": "0.1.0-beta.14",
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.13"
24
+ "@xhub-short/contracts": "0.1.0-beta.14"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsup": "^8.3.0",
28
28
  "typescript": "^5.7.0",
29
29
  "vitest": "^2.1.0",
30
- "@xhub-short/tsconfig": "0.0.1-beta.1",
31
- "@xhub-short/vitest-config": "0.1.0-beta.12"
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",