@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 +11 -0
- package/dist/index.js +92 -27
- package/package.json +4 -4
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
|
-
|
|
2681
|
-
|
|
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 =
|
|
2758
|
-
if (action
|
|
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
|
-
|
|
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 (
|
|
2868
|
-
else if (
|
|
2869
|
-
|
|
2870
|
-
else if (
|
|
2871
|
-
|
|
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(
|
|
2874
|
-
this.applyRollback(
|
|
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.
|
|
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.
|
|
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/
|
|
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",
|