use promise to cache requests
Don't do N equal requests for N equal answers
This commit is contained in:
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
|
// Тип для ответа сервера, который мы ожидаем
|
||||||
|
type AllReactionsForIssue = {
|
||||||
|
[commentId: string]: {
|
||||||
|
[emoji: string]: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- НАШ КЭШ ---
|
||||||
|
// Мы будем хранить Promise, а не сами данные.
|
||||||
|
let reactionsPromiseCache: Promise<AllReactionsForIssue> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все реакции для указанной задачи.
|
||||||
|
* Использует кэш на уровне модуля, чтобы избежать повторных запросов на одной странице.
|
||||||
|
*/
|
||||||
|
export async function fetchReactionsForIssue(issueId: number): Promise<AllReactionsForIssue> {
|
||||||
|
// Если Promise уже есть в кэше, просто возвращаем его.
|
||||||
|
if (reactionsPromiseCache) {
|
||||||
|
console.log('Serving reactions from cache.');
|
||||||
|
return reactionsPromiseCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching reactions from network...');
|
||||||
|
|
||||||
|
// Если кэш пуст, создаем новый Promise, СОХРАНЯЕМ ЕГО В КЭШ, и возвращаем.
|
||||||
|
reactionsPromiseCache = fetch(`${API_BASE_URL}/api/reactions/${issueId}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Network response was not ok: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// В случае ошибки, очищаем кэш, чтобы можно было попробовать снова.
|
||||||
|
reactionsPromiseCache = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
return reactionsPromiseCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает анонимный ID пользователя из хранилища.
|
||||||
|
* (Переносим эту логику сюда, чтобы весь код, связанный с API/Storage, был в одном месте).
|
||||||
|
*/
|
||||||
|
export async function getAnonymousUserId(): Promise<string> {
|
||||||
|
const data = await browser.storage.local.get('userId');
|
||||||
|
if (data.userId) return data.userId;
|
||||||
|
const newUserId = crypto.randomUUID();
|
||||||
|
await browser.storage.local.set({ userId: newUserId });
|
||||||
|
return newUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет запрос на добавление/удаление реакции.
|
||||||
|
*/
|
||||||
|
export async function toggleReactionApi(
|
||||||
|
issueId: number,
|
||||||
|
commentId: string,
|
||||||
|
emoji: string,
|
||||||
|
userId: string,
|
||||||
|
method: 'POST' | 'DELETE'
|
||||||
|
) {
|
||||||
|
return fetch(`${API_BASE_URL}/api/reactions`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-ID': userId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ issueId, commentId, emoji }),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react';
|
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react';
|
||||||
import browser from 'webextension-polyfill';
|
import { fetchReactionsForIssue, getAnonymousUserId, toggleReactionApi } from '../api';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
|
||||||
|
|
||||||
type CommentReactions = { [emoji: string]: string[] };
|
type CommentReactions = { [emoji: string]: string[] };
|
||||||
interface ReactionsProps { issueId: number; commentId: string; }
|
interface ReactionsProps { issueId: number; commentId: string; }
|
||||||
|
|
||||||
async function getAnonymousUserId(): Promise<string> {
|
|
||||||
const data = await browser.storage.local.get('userId');
|
|
||||||
if (data.userId) return data.userId;
|
|
||||||
const newUserId = crypto.randomUUID();
|
|
||||||
await browser.storage.local.set({ userId: newUserId });
|
|
||||||
return newUserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Reactions: React.FC<ReactionsProps> = ({ issueId, commentId }) => {
|
const Reactions: React.FC<ReactionsProps> = ({ issueId, commentId }) => {
|
||||||
const [reactions, setReactions] = useState<CommentReactions>({});
|
const [reactions, setReactions] = useState<CommentReactions>({});
|
||||||
const [myUserId, setMyUserId] = useState<string | null>(null);
|
const [myUserId, setMyUserId] = useState<string | null>(null);
|
||||||
@@ -25,16 +15,17 @@ const Reactions: React.FC<ReactionsProps> = ({ issueId, commentId }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const userId = await getAnonymousUserId();
|
const [userId, allReactions] = await Promise.all([
|
||||||
|
getAnonymousUserId(),
|
||||||
|
fetchReactionsForIssue(issueId)
|
||||||
|
]);
|
||||||
|
|
||||||
setMyUserId(userId);
|
setMyUserId(userId);
|
||||||
const response = await fetch(`${API_BASE_URL}/api/reactions/${issueId}`);
|
if (allReactions && allReactions[commentId]) {
|
||||||
if (!response.ok) throw new Error('Network response was not ok');
|
setReactions(allReactions[commentId]);
|
||||||
const data = await response.json();
|
|
||||||
if (data && data[commentId]) {
|
|
||||||
setReactions(data[commentId]);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch reactions:", error);
|
console.error("Failed to fetch data:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -52,22 +43,22 @@ const Reactions: React.FC<ReactionsProps> = ({ issueId, commentId }) => {
|
|||||||
|
|
||||||
const handleReactionClick = async (emoji: string) => {
|
const handleReactionClick = async (emoji: string) => {
|
||||||
if (!myUserId) return;
|
if (!myUserId) return;
|
||||||
const currentReactions = reactions[emoji] || [];
|
const hasReacted = (reactions[emoji] || []).includes(myUserId);
|
||||||
const hasReacted = currentReactions.includes(myUserId);
|
|
||||||
const method = hasReacted ? 'DELETE' : 'POST';
|
const method = hasReacted ? 'DELETE' : 'POST';
|
||||||
const previousReactions = { ...reactions };
|
const previousReactions = { ...reactions };
|
||||||
|
|
||||||
setReactions(prev => {
|
setReactions(prev => {
|
||||||
const newUsers = hasReacted ? (prev[emoji] || []).filter(id => id !== myUserId) : [...(prev[emoji] || []), myUserId];
|
const users = prev[emoji] || [];
|
||||||
|
const newUsers = hasReacted ? users.filter(id => id !== myUserId) : [...users, myUserId];
|
||||||
const newReactionsForComment = { ...prev, [emoji]: newUsers };
|
const newReactionsForComment = { ...prev, [emoji]: newUsers };
|
||||||
if (newReactionsForComment[emoji].length === 0) delete newReactionsForComment[emoji];
|
if (newReactionsForComment[emoji].length === 0) {
|
||||||
|
delete newReactionsForComment[emoji];
|
||||||
|
}
|
||||||
return newReactionsForComment;
|
return newReactionsForComment;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/reactions`, {
|
const response = await toggleReactionApi(issueId, commentId, emoji, myUserId, method);
|
||||||
method,
|
|
||||||
headers: { 'Content-Type': 'application/json', 'X-User-ID': myUserId },
|
|
||||||
body: JSON.stringify({ issueId, commentId, emoji }),
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Server returned an error');
|
if (!response.ok) throw new Error('Server returned an error');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update reaction:", error);
|
console.error("Failed to update reaction:", error);
|
||||||
@@ -130,5 +121,4 @@ const styles: { [key: string]: React.CSSProperties } = {
|
|||||||
skeleton: { height: '26px', backgroundColor: '#eef0f2', borderRadius: '12px', animation: 'reactions-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite' },
|
skeleton: { height: '26px', backgroundColor: '#eef0f2', borderRadius: '12px', animation: 'reactions-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite' },
|
||||||
addButtonSkeleton: { height: '26px', width: '42px', backgroundColor: 'transparent', border: '1px dashed #e0e0e0', borderRadius: '12px' },
|
addButtonSkeleton: { height: '26px', width: '42px', backgroundColor: 'transparent', border: '1px dashed #e0e0e0', borderRadius: '12px' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Reactions;
|
export default Reactions;
|
||||||
|
|||||||
Reference in New Issue
Block a user