use promise to cache requests

Don't do N equal requests for N equal answers
master
Sergey Marinkevich 4 months ago
parent b4a66ee5c0
commit dbe8418cdd

@ -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;

Loading…
Cancel
Save